Skip to content

Commit

Permalink
initial support for memory overlays (#563)
Browse files Browse the repository at this point in the history
* initial support for memory overlays

* name the option dryRun

* drafts are working

* add destroy
  • Loading branch information
mafintosh authored Sep 23, 2024
1 parent 0e555c9 commit cb077a8
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 12 deletions.
7 changes: 4 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ module.exports = class Hypercore extends EventEmitter {
this.opened = false
this.closed = false
this.snapshotted = !!opts.snapshot
this.draft = !!opts.draft
this.sparse = opts.sparse !== false
this.sessions = opts._sessions || [this]
this.autoClose = !!opts.autoClose
Expand Down Expand Up @@ -267,7 +268,7 @@ module.exports = class Hypercore extends EventEmitter {
this.writable = this._isWritable()
this.autoClose = o.autoClose

if (o.state) this.state = this.snapshotted ? o.state.snapshot() : o.state.ref()
if (o.state) this.state = this.draft ? o.state.memoryOverlay() : this.snapshotted ? o.state.snapshot() : o.state.ref()

if (o.core) this.tracer.setParent(o.core.tracer)

Expand Down Expand Up @@ -804,7 +805,7 @@ module.exports = class Hypercore extends EventEmitter {
if (this.opened === false) await this.opening
if (!isValidIndex(bytes)) throw ASSERTION('seek is invalid')

const tree = (opts && opts.tree) || this.core.tree
const tree = (opts && opts.tree) || this.state.tree
const s = tree.seek(bytes, this.padding)

const offset = await s.update()
Expand Down Expand Up @@ -1036,7 +1037,7 @@ module.exports = class Hypercore extends EventEmitter {
async treeHash (length) {
if (length === undefined) {
await this.ready()
length = this.core.tree.length
length = this.state.tree.length
}

const roots = await this.state.tree.getRoots(length)
Expand Down
34 changes: 27 additions & 7 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const hypercoreCrypto = require('hypercore-crypto')
const b4a = require('b4a')
const assert = require('nanoassert')
const unslab = require('unslab')
const MemoryOverlay = require('./memory-overlay')
const Mutex = require('./mutex')
const MerkleTree = require('./merkle-tree')
const BlockStore = require('./block-store')
Expand Down Expand Up @@ -116,7 +117,7 @@ class Update {
}

class SessionState {
constructor (core, storage, blocks, tree, bitfield, treeLength, snapshot) {
constructor (core, storage, blocks, tree, bitfield, treeLength, snapshot, overlay) {
this.core = core

this.storage = storage
Expand All @@ -127,6 +128,7 @@ class SessionState {
this.blocks = blocks
this.tree = tree
this.bitfield = bitfield
this.overlay = overlay

this.treeLength = treeLength

Expand Down Expand Up @@ -175,14 +177,32 @@ class SessionState {
this.tree,
this.bitfield,
this.treeLength,
snapshot
snapshot,
null
)

return s
}

memoryOverlay () {
const storage = new MemoryOverlay(this.storage)
const s = new SessionState(
this.core,
this.storage,
this.blocks,
this.tree.clone(storage),
this.bitfield,
this.treeLength,
null,
storage
)

return s
}

createReadBatch () {
return this.storage.createReadBatch({ snapshot: this.storageSnapshot })
const reader = this.storage.createReadBatch({ snapshot: this.storageSnapshot })
return this.overlay === null ? reader : this.overlay.createReadBatch(reader)
}

_clearActiveBatch (err) {
Expand All @@ -200,7 +220,7 @@ class SessionState {
createUpdate () {
assert(!this._activeBatch && !this.isSnapshot)

this._activeBatch = this.storage.createWriteBatch()
this._activeBatch = this.overlay ? this.overlay.createWriteBatch() : this.storage.createWriteBatch()
return new Update(this._activeBatch, this.bitfield, this.core.header, this)
}

Expand Down Expand Up @@ -429,7 +449,7 @@ module.exports = class Core {
this.sessions = sessions
this.globalCache = globalCache

this.state = new SessionState(this, storage, this.blocks, tree, bitfield, tree.length, null)
this.state = new SessionState(this, storage, this.blocks, tree, bitfield, tree.length, null, null)

this._manifestFlushed = !!header.manifest
this._maxOplogSize = 65536
Expand Down Expand Up @@ -458,7 +478,7 @@ module.exports = class Core {
length: (length === treeLength || !treeInfo) ? treeLength : treeInfo.length
})

return new SessionState(this, storage, this.blocks, tree, bitfield, treeLength, null)
return new SessionState(this, storage, this.blocks, tree, bitfield, treeLength, null, null)
}

static async open (db, opts = {}) {
Expand Down Expand Up @@ -853,7 +873,7 @@ module.exports = class Core {

const promises = []

const reader = state.storage.createReadBatch()
const reader = state.createReadBatch()
for (let i = treeLength; i < length; i++) promises.push(reader.getBlock(i))
reader.tryFlush()

Expand Down
256 changes: 256 additions & 0 deletions lib/memory-overlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
const b4a = require('b4a')
const { ASSERTION } = require('hypercore-errors')

class MemoryOverlay {
constructor (storage) {
this.storage = storage
this.head = null
this.auth = null
this.localKeyPair = null
this.encryptionKey = null
this.dataInfo = null
this.userData = null
this.blocks = null
this.treeNodes = null
this.bitfields = null
}

createReadBatch (read = this.storage.createReadBatch()) {
return new MemoryOverlayReadBatch(this, read)
}

createWriteBatch () {
return new MemoryOverlayWriteBatch(this)
}

merge (overlay) {
if (overlay.head !== null) this.head = overlay.head
if (overlay.auth !== null) this.auth = overlay.auth
if (overlay.localKeyPair !== null) this.localKeyPair = overlay.localKeyPair
if (overlay.encryptionKey !== null) this.encryptionKey = overlay.encryptionKey
if (overlay.dataInfo !== null) this.dataInfo = overlay.dataInfo
if (overlay.userData !== null) this.userData = mergeMap(this.userData, overlay.userData)
if (overlay.blocks !== null) this.blocks = mergeTip(this.blocks, overlay.blocks)
if (overlay.treeNodes !== null) this.treeNodes = mergeMap(this.treeNodes, overlay.treeNodes)
if (overlay.bitfields !== null) this.bitfields = mergeTip(this.bitfields, overlay.bitfields)
}
}

class TipList {
constructor () {
this.offset = 0
this.data = []
}

end () {
return this.offset + this.data.length
}

put (index, value) {
if (this.data.length === 0) {
this.offset = index
this.data.push(value)
return
}

if (this.data.length === index) {
this.push.push(value)
return
}

throw ASSERTION('Invalid put on tip list')
}

get (index) {
index -= this.offset
if (index >= this.data.length) return null
return this.data[index]
}

* [Symbol.iterator] () {
for (let i = 0; i < this.data.length; i++) {
yield [i + this.offset, this.data[i]]
}
}
}

module.exports = MemoryOverlay

class MemoryOverlayReadBatch {
constructor (overlay, read) {
this.read = read
this.overlay = overlay
}

async getCoreHead () {
return this.overlay.head !== null ? this.overlay.head : this.read.getCoreHead()
}

async getCoreAuth () {
return this.overlay.auth !== null ? this.overlay.auth : this.read.getCoreAuth()
}

async getLocalKeyPair () {
return this.overlay.localKeyPair !== null ? this.overlay.localKeyPair : this.read.getLocalKeyPair()
}

async getEncryptionKey () {
return this.overlay.encryptionKey !== null ? this.overlay.encryptionKey : this.read.getEncryptionKey()
}

async getDataInfo () {
return this.overlay.dataInfo !== null ? this.overlay.dataInfo : this.read.getDataInfo()
}

async getUserData (key) {
const hex = this.overlay.userData === null ? null : b4a.toString('hex', key)
return hex !== null && this.userData.has(hex) ? this.overlay.dataInfo.get(hex) : this.read.getUserData(key)
}

async hasBlock (index) {
if (this.overlay.blocks !== null && index >= this.overlay.blocks.offset) {
const blk = this.overlay.blocks.get(index)
if (blk !== null) return true
}
return this.read.hasBlock(index)
}

async getBlock (index, error) {
if (this.overlay.blocks !== null && index >= this.overlay.blocks.offset) {
const blk = this.overlay.blocks.get(index)
if (blk !== null) return blk
}
return this.read.getBlock(index, error)
}

async hasTreeNode (index) {
return (this.overlay.treeNodes !== null && this.overlay.treeNodes.has(index)) || this.read.hasTreeNode(index)
}

async getTreeNode (index, error) {
if (this.overlay.treeNodes !== null && this.overlay.treeNodes.has(index)) {
return this.overlay.treeNodes.get(index)
}
return this.read.getTreeNode(index, error)
}

async getBitfieldPage (index) {
if (this.overlay.bitfields !== null && index >= this.overlay.bitfields.offset) {
const page = this.overlay.bitfields.get(index)
if (page !== null) return page
}
return this.read.getBitfieldPage(index)
}

destroy () {
this.read.destroy()
}

flush () {
return this.read.flush()
}

tryFlush () {
this.read.tryFlush()
}
}

class MemoryOverlayWriteBatch {
constructor (storage) {
this.storage = storage
this.overlay = new MemoryOverlay()
}

setCoreHead (head) {
this.overlay.head = head
}

setCoreAuth (auth) {
this.overlay.auth = auth
}

setBatchPointer (name, pointer) {
throw ASSERTION('Not supported')
}

setDataDependency (dataInfo) {
throw ASSERTION('Not supported')
}

setLocalKeyPair (keyPair) {
this.overlay.localKeyPair = keyPair
}

setEncryptionKey (encryptionKey) {
this.overlay.encryptionKey = encryptionKey
}

setDataInfo (info) {
this.overlay.dataInfo = info
}

setUserData (key, value) {
if (this.overlay.userData === null) this.overlay.userData = new Map()
this.overlay.userData.set(b4a.toString(key, 'hex'), value)
}

putBlock (index, data) {
if (this.overlay.blocks === null) this.overlay.blocks = new TipList()
this.overlay.blocks.put(index, data)
}

deleteBlock (index) {
throw ASSERTION('Not supported yet, but will be')
}

deleteBlockRange (start, end) {
throw ASSERTION('Not supported yet, but will be')
}

putTreeNode (node) {
if (this.overlay.treeNodes === null) this.overlay.treeNodes = new Map()
this.overlay.treeNodes.set(node.index, node)
}

deleteTreeNode (index) {
throw ASSERTION('Not supported yet, but will be')
}

deleteTreeNodeRange (start, end) {
throw ASSERTION('Not supported yet, but will be')
}

putBitfieldPage (index, page) {
if (this.overlay.bitfields === null) this.overlay.bitfields = new TipList()
this.overlay.bitfields.put(index, page)
}

deleteBitfieldPage (index) {
throw new Error('Not supported yet, but will be')
}

deleteBitfieldPageRange (start, end) {
throw new Error('Not supported yet, but will be')
}

destroy () {}

flush () {
this.storage.merge(this.overlay)
return Promise.resolve()
}
}

function mergeMap (a, b) {
if (a === null) return b
for (const [key, value] of b) a.set(key, value)
return a
}

function mergeTip (a, b) {
if (a === null) return b
while (a.end() !== b.offset && b.offset >= a.offset && b.end() >= a.end()) a.data.pop()
if (a.end() !== b.offset) throw ASSERTION('Cannot merge tip list')
for (const data of b.data) a.data.push(data)
return a
}
Loading

0 comments on commit cb077a8

Please sign in to comment.