From f2727845a0346d94549325e46d33b245cb946fbe Mon Sep 17 00:00:00 2001 From: Mathias Buus Date: Mon, 23 Sep 2024 12:35:29 +0200 Subject: [PATCH] drafts are working --- index.js | 8 +++---- lib/core.js | 7 +++--- lib/memory-overlay.js | 15 +++++++++++-- lib/merkle-tree.js | 15 +++++++++++-- test/all.js | 1 + test/draft.js | 50 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 test/draft.js diff --git a/index.js b/index.js index 2ac1ced6..f3c9af08 100644 --- a/index.js +++ b/index.js @@ -77,7 +77,7 @@ module.exports = class Hypercore extends EventEmitter { this.opened = false this.closed = false this.snapshotted = !!opts.snapshot - this.dryRun = !!opts.dryRun + this.draft = !!opts.draft this.sparse = opts.sparse !== false this.sessions = opts._sessions || [this] this.autoClose = !!opts.autoClose @@ -268,7 +268,7 @@ module.exports = class Hypercore extends EventEmitter { this.writable = this._isWritable() this.autoClose = o.autoClose - if (o.state) this.state = this.dryRun ? o.state.memoryOverlay() : 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) @@ -805,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() @@ -1037,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) diff --git a/lib/core.js b/lib/core.js index 38473f5a..5fd77cd4 100644 --- a/lib/core.js +++ b/lib/core.js @@ -185,15 +185,16 @@ class SessionState { } memoryOverlay () { + const storage = new MemoryOverlay(this.storage) const s = new SessionState( this.core, this.storage, this.blocks, - this.tree, + this.tree.clone(storage), this.bitfield, this.treeLength, null, - new MemoryOverlay(this.storage) + storage ) return s @@ -871,7 +872,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() diff --git a/lib/memory-overlay.js b/lib/memory-overlay.js index 675d7f18..47ec988a 100644 --- a/lib/memory-overlay.js +++ b/lib/memory-overlay.js @@ -15,7 +15,7 @@ class MemoryOverlay { this.bitfields = null } - createReadBatch (read) { + createReadBatch (read = this.storage.createReadBatch()) { return new MemoryOverlayReadBatch(this, read) } @@ -42,6 +42,10 @@ class TipList { this.data = [] } + end () { + return this.offset + this.data.length + } + put (index, value) { if (this.data.length === 0) { this.offset = index @@ -62,6 +66,12 @@ class TipList { 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 @@ -233,7 +243,8 @@ function mergeMap (a, b) { function mergeTip (a, b) { if (a === null) return b - if (a.offset + a.data.length !== b.offset) throw ASSERTION('Cannot merge tip list') + 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 } diff --git a/lib/merkle-tree.js b/lib/merkle-tree.js index 23c4a644..09a0febf 100644 --- a/lib/merkle-tree.js +++ b/lib/merkle-tree.js @@ -509,6 +509,10 @@ module.exports = class MerkleTree { return u.finalise() } + clone (storage) { + return new MerkleTree(storage, this.roots.slice(0), this.fork, this.signature, this.prologue) + } + batch () { return new MerkleTreeBatch(this) } @@ -683,7 +687,7 @@ module.exports = class MerkleTree { get (index, error = true, readBatch = null) { if (readBatch) return readBatch.getTreeNode(index, error) - return this.storage.getTreeNode(index, error) + return getTreeNode(this.storage, index, error) } clear (writer) { @@ -847,7 +851,7 @@ module.exports = class MerkleTree { const roots = [] for (const index of flat.fullRoots(2 * length)) { - roots.push(unslabNode(await storage.getTreeNode(index, true))) + roots.push(unslabNode(await getTreeNode(storage, index, true))) } return new MerkleTree(storage, roots, opts.fork || 0, opts.signature || null, opts.prologue || null) @@ -1274,6 +1278,13 @@ function normalizeIndexed (block, hash) { return null } +function getTreeNode (storage, index, error) { + const batch = storage.createReadBatch() + const node = batch.getTreeNode(index, error) + batch.tryFlush() + return node +} + async function settleProof (p) { const result = [ p.node && Promise.all(p.node), diff --git a/test/all.js b/test/all.js index 0d710408..acc2b4f2 100644 --- a/test/all.js +++ b/test/all.js @@ -14,6 +14,7 @@ async function runTests () { // await import('./compat.js') // todo: how to test compat? await import('./conflicts.js') await import('./core.js') + await import('./draft.js') await import('./encodings.js') await import('./encryption.js') await import('./extension.js') diff --git a/test/draft.js b/test/draft.js new file mode 100644 index 00000000..15806f43 --- /dev/null +++ b/test/draft.js @@ -0,0 +1,50 @@ +const { create } = require('./helpers') +const test = require('brittle') +const b4a = require('b4a') + +test('draft', async function (t) { + const core = await create(t) + + await core.append('hello') + await core.append('world') + + const draft = core.session({ draft: true }) + + await draft.append('edits!') + + t.alike(await draft.get(0), b4a.from('hello')) + t.alike(await draft.get(1), b4a.from('world')) + t.alike(await draft.get(2), b4a.from('edits!')) + t.alike(await draft.seek(11), [2, 1]) + t.alike(draft.byteLength, 16) + t.alike(draft.length, 3) + + await draft.close() + + // nothing changed as it was a draft + t.alike(core.byteLength, 10) + t.alike(core.length, 2) + + await core.close() +}) + +test('draft and then undraft', async function (t) { + const core = await create(t) + + await core.append('hello') + await core.append('world') + + const draft = core.session({ draft: true }) + + await draft.append('edits!') + + await core.core.commit(draft.state) + + await draft.close() + + // nothing changed as it was a draft + t.alike(core.byteLength, 16) + t.alike(core.length, 3) + + await core.close() +})