const rrp = require('resolve-reject-promise')
const safetyCatch = require('safety-catch')
const SystemView = require('./system.js')
const { EncryptionView } = require('./encryption.js')
const DEFAULT_OP_TIMEOUT = 5_000
const DEFAULT_MIN_FF = 16
module.exports = class FastForward {
constructor(
base,
key,
{
timeout = DEFAULT_OP_TIMEOUT,
verified = true,
minimum = DEFAULT_MIN_FF,
force = false,
length = 0
} = {}
) {
this.base = base
this.key = key
this.timeout = timeout
this.force = force
this.core = base.store.get({ key, active: true, encryption: null })
this.length = length
this.views = []
this.encryption = null
this.entropy = null
this.indexers = []
this.minimum = minimum
this.verified = verified
this.waiting = new Set()
this.cores = [this.core]
this.system = null
this.destroyed = false
this.upgrading = null
this.failed = false
}
static MINIMUM = DEFAULT_MIN_FF
async upgrade() {
try {
if (!this.upgrading) this.upgrading = this._upgrade()
if (!(await this.upgrading)) return null
return {
length: this.length,
force: this.force,
key: this.key,
indexers: this.indexers,
views: this.views,
entropy: this.entropy,
manifestVersion: this.core.manifest.version,
minimum: this.minimum
}
} catch (err) {
safetyCatch(err)
this.failed = true
return null
} finally {
await this.close()
}
}
_waitForAppend(core) {
if (core.length > 0) return Promise.resolve()
const promise = rrp()
const timeout = setTimeout(this.close.bind(this), this.timeout)
const wait = { promise, timeout }
this.waiting.add(wait)
core.once('append', () => {
this.waiting.delete(wait)
clearTimeout(timeout)
promise.resolve()
})
return promise.promise
}
_minLength() {
return this.base.core.length + this.minimum
}
async _upgrade() {
await this.core.ready()
if (!this.verified) await this._waitForAppend(this.core)
if (this.destroyed) return false
if (this.core.manifest.linked) {
const appends = []
for (const key of this.core.manifest.linked) {
const core = this.base.store.get({ key, active: true })
this.cores.push(core)
await core.ready()
appends.push(this._waitForAppend(core))
}
await Promise.all(appends)
}
if (this.length === 0) this.length = this.core.length
// note we use the persisted length here, as we might as well continue that work as thats ~ the same length
if (!this.force && this.length < this._minLength()) return false
if (this.length === 0) return false
await this.core.get(this.length - 1, { timeout: this.timeout })
await this.base.ready()
if (this.destroyed) return false
if (this.base.encryptionKey && !(await this._ensureEncryption())) return false
if (this.destroyed) return false
this.system = new SystemView(this.core, { checkout: this.length })
await this.system.ready()
if (this.destroyed) return false
this.entropy = this.system.entropy
if (this.core.manifest.version >= 2) {
const linked = this.core.manifest.linked
if (this.system.encryptionLength === 0 || linked === null || !linked.length) {
return false
}
// technically diverges from system.views to include encryption here, but makes sense
this.views.push({
key: linked[0],
length: this.system.encryptionLength
})
}
const promises = []
// ensure local key is locally available always
promises.push(this.system.get(this.base.local.key, { timeout: this.timeout }))
if (this.encryption && this.encryption.core) {
promises.push(
this.encryption.core.get(this.system.encryptionLength - 1, { timeout: this.timeout })
)
}
for (const v of this.system.views) {
this.views.push(v)
const core = this.base.store.get({ key: v.key, active: true })
this.cores.push(core)
if (!v.length) continue // encryption view can be 0 length
promises.push(core.get(v.length - 1, { timeout: this.timeout }))
}
for (const idx of this.system.indexers) {
this.indexers.push(idx)
if (idx.length === 0) continue // need to make sure we have manifest...
const core = this.base.store.get({ key: idx.key, active: true })
this.cores.push(core)
promises.push(core.get(idx.length - 1, { timeout: this.timeout }))
promises.push(this.system.get(idx.key, { timeout: this.timeout }))
}
for (const head of this.system.heads) {
promises.push(this.system.get(head.key, { timeout: this.timeout }))
}
await Promise.all(promises)
if (this.destroyed) return false
return true
}
async _ensureEncryption() {
// only need encryption if we migrated
if (this.core.encryption !== null) return true
const encCore = this.core.manifest.version <= 1 ? null : this.cores[1]
// expects internal views to be loaded in order
this.encryption = new EncryptionView(this.base, encCore)
const encryption = this.encryption.getViewEncryption('_system')
await this.core.setEncryption(encryption)
return true
}
async close() {
this.destroyed = true
for (const { promise, timeout } of this.waiting) {
clearTimeout(timeout)
promise.resolve() // silently bail
}
if (this.system) await this.system.close()
if (this.encryption) await this.encryption.close()
for (const core of this.cores) await core.close()
await this.core.close()
}
}