From 01bcfcc64d84b9269d087bb4c6b916e26f7533f9 Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Fri, 26 Jun 2026 11:13:45 -0400 Subject: [PATCH 1/9] add localOnly to createReadStream --- index.js | 7 +++--- lib/context.js | 15 ++++++++++--- lib/inflate.js | 36 +++++++++++++++++++++++++++--- lib/ranges.js | 52 +++++++++++++++++++++++++++++++++---------- lib/session-config.js | 20 +++++++++++------ test/basic.js | 41 +++++++++++++++++++++++++++------- 6 files changed, 135 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index ffc7448..e97b013 100644 --- a/index.js +++ b/index.js @@ -220,15 +220,16 @@ class Hyperbee extends EventEmitter { } async inflate(ptr, config) { - if (!ptr.value) { - await inflate(ptr, config) - } + const value = await inflate(ptr, config) + if (!value) return null + this.bump(ptr) return ptr.value } async finalizeKeyPointer(key, config) { const value = key.value || (await inflateValue(key, config)) + if (value === null && config.localOnly) return null return { core: key.context.getCore(key.core), diff --git a/lib/context.js b/lib/context.js index 76bea31..c4a9fb2 100644 --- a/lib/context.js +++ b/lib/context.js @@ -163,11 +163,17 @@ class CoreContext { } async getBlock(seq, core, config) { - if (core !== 0 && core - 1 >= this.cores.length) await this.update(config) + if (core !== 0 && core - 1 >= this.cores.length) { + if (config.localOnly) return null + await this.update(config) + } const hc = this.getCore(core) const buffer = await hc.get(seq, config) - if (buffer === null) throw BLOCK_NOT_AVAILABLE() + if (buffer === null) { + if (config.localOnly) return null + throw BLOCK_NOT_AVAILABLE() + } if (config.trace !== null) config.trace(core, seq) @@ -194,7 +200,10 @@ class CoreContext { async getContext(core, config) { if (core === 0) return this - if (core > this.cores.length) await this.update(config) + if (core > this.cores.length) { + if (config.localOnly) return null + await this.update(config) + } if (core > this.cores.length) throw new Error('Bad core index: ' + core) const hex = b4a.toString(this.cores[core - 1].key, 'hex') diff --git a/lib/inflate.js b/lib/inflate.js index 8f5bcce..85e5b48 100644 --- a/lib/inflate.js +++ b/lib/inflate.js @@ -9,6 +9,7 @@ exports.inflate = async function inflate(ptr, config) { ptr.context.getBlock(ptr.seq, ptr.core, config), ptr.context.getContext(ptr.core, config) ]) + if (config.localOnly && (!block || !context)) return null const tree = block.tree[ptr.offset] @@ -32,6 +33,10 @@ exports.inflate = async function inflate(ptr, config) { return ptr.value } +function missingDelta(d) { + return new DeltaOp(false, d.type, d.index, null) +} + function inflateKey(context, d, ptr, block, config) { if (d.type === OP_COHORT) return inflateKeyCohort(context, d, ptr, block, config) return inflateKeyDelta(context, d, ptr, block, config) @@ -40,13 +45,15 @@ function inflateKey(context, d, ptr, block, config) { async function inflateKeyDelta(context, d, ptr, block, config) { const k = d.pointer - if (!k) return new DeltaOp(false, d.type, d.index, null) + if (!k) return missingDelta(d) const blk = k.seq === ptr.seq && k.core === 0 && ptr.core === 0 ? block : await context.getBlock(k.seq, k.core, config) + if (config.localOnly && !blk) return missingDelta(d) + const bk = blk.keys[k.offset] let vp = null @@ -54,6 +61,7 @@ async function inflateKeyDelta(context, d, ptr, block, config) { if (bk.valuePointer) { const p = bk.valuePointer const ctx = await context.getContext(k.core, config) + if (config.localOnly && !ctx) return missingDelta(d) vp = new ValuePointer(ctx, p.core, p.seq, p.offset, p.split) } @@ -69,6 +77,7 @@ exports.inflateValue = async function inflateValue(key, config) { if (ptr.split === 0) { const block = await ptr.context.getBlock(ptr.seq, ptr.core, config) + if (config.localOnly && !block) return null return block.values[ptr.offset] } @@ -77,6 +86,10 @@ exports.inflateValue = async function inflateValue(key, config) { blockPromises[i] = ptr.context.getBlock(ptr.seq - ptr.split + i, ptr.core, config) } const blocks = await Promise.all(blockPromises) + + // if any block is missing, treat the whole value as missing. + if (config.localOnly && blocks.includes(null)) return null + const splitValue = new Array(blockPromises.length) for (let i = 0; i < splitValue.length - 1; i++) { splitValue[i] = blocks[i].values[0] @@ -93,9 +106,12 @@ async function inflateKeyCohort(context, d, ptr, block, config) { ? block : await context.getBlock(co.seq, co.core, config) + if (config.localOnly && !blk) return missingDelta(d) + const cohort = blk.cohorts[co.offset] const promises = new Array(cohort.length) const ctx = await context.getContext(co.core, config) + if (config.localOnly && !ctx) return missingDelta(d) for (let i = 0; i < cohort.length; i++) { const p = cohort[i] @@ -109,7 +125,11 @@ async function inflateKeyCohort(context, d, ptr, block, config) { async function inflateChild(context, d, ptr, block, config) { if (d.type === OP_COHORT) return inflateChildCohort(context, d, ptr, block, config) - if (d.pointer && !context.hasCore(d.pointer.core)) await context.update(config) + const missingCore = d.pointer && !context.hasCore(d.pointer.core) + + if (config.localOnly && missingCore) return missingDelta(d) + else if (missingCore) await context.update(config) + return inflateChildDelta(context, d, ptr, block, config) } @@ -127,13 +147,23 @@ async function inflateChildCohort(context, d, ptr, block, config) { ? block : await context.getBlock(co.seq, co.core, config) + if (config.localOnly && !blk) return missingDelta(d) + const cohort = blk.cohorts[co.offset] const deltas = new Array(cohort.length) const ctx = await context.getContext(co.core, config) + if (config.localOnly && !ctx) return missingDelta(d) for (let i = 0; i < cohort.length; i++) { const c = cohort[i] - if (c.pointer && !ctx.hasCore(c.pointer.core)) await ctx.update(config) + if (config.localOnly) { + if (c.pointer && !ctx.hasCore(c.pointer.core)) { + deltas[i] = missingDelta(c) + continue + } + } else if (!ctx.hasCore(c.pointer.core)) { + await ctx.update(config) + } deltas[i] = inflateChildDelta(ctx, c, co, blk, config) } diff --git a/lib/ranges.js b/lib/ranges.js index fe6b9a5..831bc8f 100644 --- a/lib/ranges.js +++ b/lib/ranges.js @@ -25,7 +25,7 @@ class RangeIterator { this.end = lte || lt || null this.inclusive = !!(reverse ? lte : gte) this.compare = (reverse ? gte : lte) ? 0 : -1 - this.prefetch = prefetch + this.prefetch = prefetch && !this.config.localOnly this.prefetching = null } @@ -46,13 +46,20 @@ class RangeIterator { const v = top.node.value ? this.tree.bump(top.node) : await this.tree.inflate(top.node, this.config) + if (!v) { + this.stack.pop() + if (this.stack.length == 0) break + continue + } for (let i = 0; i < v.keys.length; i++) { const j = this.reverse ? v.keys.length - 1 - i : i + const key = v.keys.get(j) - const c = this.reverse - ? b4a.compare(v.keys.get(j).key, this.end) - : b4a.compare(this.start, v.keys.get(j).key) + // skip non-local keys + if (this.config.localOnly && !key) continue + + const c = this.reverse ? b4a.compare(key.key, this.end) : b4a.compare(this.start, key.key) if (c < 0) break top.offset = 2 * i + 1 + (c === 0 ? offset : 1) @@ -64,19 +71,32 @@ class RangeIterator { if (!child || k >= v.children.length) break const j = this.reverse ? v.children.length - 1 - k : k + const node = v.children.get(j) - this.stack.push({ - node: v.children.get(j), - offset: 0 - }) + if (!this.config.localOnly || node) { + this.stack.push({ node, offset: 0 }) + } top.offset++ + if (this.config.localOnly && !node) break } } async next() { - const key = await this.nextKey() - return key === null ? key : this.tree.finalizeKeyPointer(key, this.config) + let key = await this.nextKey() + if (key === null) return null + + if (!this.config.localOnly) { + return this.tree.finalizeKeyPointer(key, this.config) + } + + // for local read-stream, iterate until we find a non null entry or run out of keys + while (true) { + const entry = await this.tree.finalizeKeyPointer(key, this.config) + if (entry !== null) return entry + key = await this.nextKey() + if (key === null) return null + } } async nextKey() { @@ -85,6 +105,7 @@ class RangeIterator { const v = top.node.value ? this.tree.bump(top.node) : await this.tree.inflate(top.node, this.config) + if (!v) continue const offset = top.offset++ const child = (offset & 1) === 0 @@ -94,16 +115,23 @@ class RangeIterator { this.stack.push(top) if (k < v.children.length) { const j = this.reverse ? v.children.length - 1 - k : k - this.stack.push({ node: v.children.get(j), offset: 0 }) + const node = v.children.get(j) + if (!this.config.localOnly || node) { + this.stack.push({ node, offset: 0 }) + } } continue } if (k < v.keys.length) { const j = this.reverse ? v.keys.length - 1 - k : k - const data = v.keys.get(j) + if (this.config.localOnly && !data) { + this.stack.push(top) + continue + } + const c = this.reverse ? this.start ? b4a.compare(this.start, data.key) diff --git a/lib/session-config.js b/lib/session-config.js index adce55f..3d5510f 100644 --- a/lib/session-config.js +++ b/lib/session-config.js @@ -1,33 +1,39 @@ class SessionConfig { - constructor(activeRequests, timeout, wait, trace) { + constructor(activeRequests, timeout, wait, trace, localOnly = false) { this.activeRequests = activeRequests this.timeout = timeout this.wait = wait this.trace = trace + this.localOnly = localOnly } - sub(activeRequests, timeout, wait, trace) { + sub(activeRequests, timeout, wait, trace, localOnly) { if ( this.activeRequests === activeRequests && this.timeout === timeout && this.wait === wait && - this.trace === trace + this.trace === trace && + this.localOnly === localOnly ) { return this } - return new SessionConfig(activeRequests, timeout, wait, trace) + return new SessionConfig(activeRequests, timeout, wait, trace, localOnly) } options(opts) { if (!opts) return this - const { + let { activeRequests = this.activeRequests, timeout = this.timeout, wait = this.wait, - trace = this.trace + trace = this.trace, + localOnly = this.localOnly } = opts - return this.sub(activeRequests, timeout, wait, trace) + + if (localOnly) wait = false + + return this.sub(activeRequests, timeout, wait, trace, localOnly) } detach(opts) { diff --git a/test/basic.js b/test/basic.js index e6c3f7a..f70f01b 100644 --- a/test/basic.js +++ b/test/basic.js @@ -633,10 +633,7 @@ test('emit update event after remote append to empty tree and autoUpdate = true' b4a.from('010000000c01011100010568656c6c6f0105776f726c64', 'hex') ]) - const { promise, resolve } = Promise.withResolvers() - setTimeout(resolve, 0) - - await promise + await new Promise((resolve) => setTimeout(resolve, 0)) t.alike(counter, 1) }) @@ -657,10 +654,7 @@ test('emit update event after remote append to non-empty tree and autoUpdate = t b4a.from('010000000d000001021159010100010268690102686f', 'hex') ]) - const { promise, resolve } = Promise.withResolvers() - setTimeout(resolve, 0) - - await promise + await new Promise((resolve) => setTimeout(resolve, 0)) t.alike(counter, 2) }) @@ -951,3 +945,34 @@ test('autoUpdate doesnt lose data', async function (t) { t.alike((await db.get(b4a.from('1'))).value, b4a.from('1')) t.alike((await db.get(b4a.from('2'))).value, b4a.from('2')) }) + +test('createReadStream localOnly', async function (t) { + const db = await create(t) + await db.ready() + + const db2 = await create(t, { key: db.core.key, autoUpdate: true }) + await db2.ready() + + replicate(t, db, db2) + + for (let i = 0; i < 5; i++) { + const w = db.write() + w.tryPut(b4a.from('' + i), b4a.from('' + i)) + await w.flush() + } + + // event flush + await new Promise((resolve) => setTimeout(resolve, 100)) + + await db2.core.download({ start: 1, end: 2 }).done() + await db2.core.download({ start: 3, end: 4 }).done() + await db2.core.download({ start: 4, end: 5 }).done() + db2.cache.empty() + + const actual = [] + for await (const data of db2.createReadStream({ localOnly: true })) { + actual.push(data.key) + } + + t.alike(actual, [b4a.from('1'), b4a.from('3'), b4a.from('4')]) +}) From cd7265b910b443dd7036348762c3f0147f3df111 Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Fri, 26 Jun 2026 11:28:11 -0400 Subject: [PATCH 2/9] Update ranges.js --- lib/ranges.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ranges.js b/lib/ranges.js index 831bc8f..9d0630d 100644 --- a/lib/ranges.js +++ b/lib/ranges.js @@ -48,7 +48,7 @@ class RangeIterator { : await this.tree.inflate(top.node, this.config) if (!v) { this.stack.pop() - if (this.stack.length == 0) break + if (this.stack.length === 0) break continue } From 901687eb6e8e97093e9b469de5ea0101c87d9aeb Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Fri, 26 Jun 2026 12:59:46 -0400 Subject: [PATCH 3/9] Update basic.js --- test/basic.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/basic.js b/test/basic.js index f70f01b..814e83b 100644 --- a/test/basic.js +++ b/test/basic.js @@ -961,18 +961,30 @@ test('createReadStream localOnly', async function (t) { await w.flush() } - // event flush await new Promise((resolve) => setTimeout(resolve, 100)) - await db2.core.download({ start: 1, end: 2 }).done() - await db2.core.download({ start: 3, end: 4 }).done() - await db2.core.download({ start: 4, end: 5 }).done() + await db2.download() + + await db2.core.startMarking() + await db2.core.markBlock(1, 2) + await db2.core.markBlock(3, 4) + await db2.core.sweep() + + // to block 3 + db2.move({ length: 4 }) db2.cache.empty() + t.not(await db2.core.has(0)) + t.not(await db2.core.has(2)) + t.not(await db2.core.has(4)) + t.ok(await db2.core.has(1)) + t.ok(await db2.core.has(3)) + const actual = [] for await (const data of db2.createReadStream({ localOnly: true })) { actual.push(data.key) } - t.alike(actual, [b4a.from('1'), b4a.from('3'), b4a.from('4')]) + // local view of the tree ( at block 3 height ) + t.alike(actual, [b4a.from('0'), b4a.from('1'), b4a.from('2'), b4a.from('3')]) }) From 1b7e30aa582a80c26cc1af31d3553d2473a631c1 Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Mon, 29 Jun 2026 16:03:17 -0400 Subject: [PATCH 4/9] localOnly -> wait: false --- index.js | 10 +++++++++- lib/context.js | 6 +++--- lib/inflate.js | 22 +++++++++++----------- lib/ranges.js | 14 +++++++------- lib/session-config.js | 19 +++++++------------ test/basic.js | 18 +++++------------- 6 files changed, 42 insertions(+), 47 deletions(-) diff --git a/index.js b/index.js index e97b013..1ca84c5 100644 --- a/index.js +++ b/index.js @@ -229,7 +229,7 @@ class Hyperbee extends EventEmitter { async finalizeKeyPointer(key, config) { const value = key.value || (await inflateValue(key, config)) - if (value === null && config.localOnly) return null + if (value === null && !config.wait) return null return { core: key.context.getCore(key.core), @@ -289,6 +289,7 @@ class Hyperbee extends EventEmitter { while (true) { const v = ptr.value ? this.bump(ptr) : await this.inflate(ptr, config) + if (!v) return null let s = 0 let e = v.keys.length @@ -298,6 +299,12 @@ class Hyperbee extends EventEmitter { const mid = (s + e) >> 1 const m = v.keys.get(mid) + if (!m) { + if (s === mid) s = mid + 1 + else e = mid + continue + } + c = b4a.compare(key, m.key) if (c === 0) return this.finalizeKeyPointer(m, config) @@ -310,6 +317,7 @@ class Hyperbee extends EventEmitter { const i = c < 0 ? e : s ptr = v.children.get(i) + if (!ptr) return null } } } diff --git a/lib/context.js b/lib/context.js index c4a9fb2..522fb79 100644 --- a/lib/context.js +++ b/lib/context.js @@ -164,14 +164,14 @@ class CoreContext { async getBlock(seq, core, config) { if (core !== 0 && core - 1 >= this.cores.length) { - if (config.localOnly) return null + if (!config.wait) return null await this.update(config) } const hc = this.getCore(core) const buffer = await hc.get(seq, config) if (buffer === null) { - if (config.localOnly) return null + if (!config.wait) return null throw BLOCK_NOT_AVAILABLE() } @@ -201,7 +201,7 @@ class CoreContext { async getContext(core, config) { if (core === 0) return this if (core > this.cores.length) { - if (config.localOnly) return null + if (!config.wait) return null await this.update(config) } if (core > this.cores.length) throw new Error('Bad core index: ' + core) diff --git a/lib/inflate.js b/lib/inflate.js index 85e5b48..f53f18e 100644 --- a/lib/inflate.js +++ b/lib/inflate.js @@ -9,7 +9,7 @@ exports.inflate = async function inflate(ptr, config) { ptr.context.getBlock(ptr.seq, ptr.core, config), ptr.context.getContext(ptr.core, config) ]) - if (config.localOnly && (!block || !context)) return null + if (!config.wait && (!block || !context)) return null const tree = block.tree[ptr.offset] @@ -52,7 +52,7 @@ async function inflateKeyDelta(context, d, ptr, block, config) { ? block : await context.getBlock(k.seq, k.core, config) - if (config.localOnly && !blk) return missingDelta(d) + if (!config.wait && !blk) return missingDelta(d) const bk = blk.keys[k.offset] @@ -61,7 +61,7 @@ async function inflateKeyDelta(context, d, ptr, block, config) { if (bk.valuePointer) { const p = bk.valuePointer const ctx = await context.getContext(k.core, config) - if (config.localOnly && !ctx) return missingDelta(d) + if (!config.wait && !ctx) return missingDelta(d) vp = new ValuePointer(ctx, p.core, p.seq, p.offset, p.split) } @@ -77,7 +77,7 @@ exports.inflateValue = async function inflateValue(key, config) { if (ptr.split === 0) { const block = await ptr.context.getBlock(ptr.seq, ptr.core, config) - if (config.localOnly && !block) return null + if (!config.wait && !block) return null return block.values[ptr.offset] } @@ -88,7 +88,7 @@ exports.inflateValue = async function inflateValue(key, config) { const blocks = await Promise.all(blockPromises) // if any block is missing, treat the whole value as missing. - if (config.localOnly && blocks.includes(null)) return null + if (!config.wait && blocks.includes(null)) return null const splitValue = new Array(blockPromises.length) for (let i = 0; i < splitValue.length - 1; i++) { @@ -106,12 +106,12 @@ async function inflateKeyCohort(context, d, ptr, block, config) { ? block : await context.getBlock(co.seq, co.core, config) - if (config.localOnly && !blk) return missingDelta(d) + if (!config.wait && !blk) return missingDelta(d) const cohort = blk.cohorts[co.offset] const promises = new Array(cohort.length) const ctx = await context.getContext(co.core, config) - if (config.localOnly && !ctx) return missingDelta(d) + if (!config.wait && !ctx) return missingDelta(d) for (let i = 0; i < cohort.length; i++) { const p = cohort[i] @@ -127,7 +127,7 @@ async function inflateChild(context, d, ptr, block, config) { if (d.type === OP_COHORT) return inflateChildCohort(context, d, ptr, block, config) const missingCore = d.pointer && !context.hasCore(d.pointer.core) - if (config.localOnly && missingCore) return missingDelta(d) + if (!config.wait && missingCore) return missingDelta(d) else if (missingCore) await context.update(config) return inflateChildDelta(context, d, ptr, block, config) @@ -147,16 +147,16 @@ async function inflateChildCohort(context, d, ptr, block, config) { ? block : await context.getBlock(co.seq, co.core, config) - if (config.localOnly && !blk) return missingDelta(d) + if (!config.wait && !blk) return missingDelta(d) const cohort = blk.cohorts[co.offset] const deltas = new Array(cohort.length) const ctx = await context.getContext(co.core, config) - if (config.localOnly && !ctx) return missingDelta(d) + if (!config.wait && !ctx) return missingDelta(d) for (let i = 0; i < cohort.length; i++) { const c = cohort[i] - if (config.localOnly) { + if (!config.wait) { if (c.pointer && !ctx.hasCore(c.pointer.core)) { deltas[i] = missingDelta(c) continue diff --git a/lib/ranges.js b/lib/ranges.js index 9d0630d..0fbaaf1 100644 --- a/lib/ranges.js +++ b/lib/ranges.js @@ -25,7 +25,7 @@ class RangeIterator { this.end = lte || lt || null this.inclusive = !!(reverse ? lte : gte) this.compare = (reverse ? gte : lte) ? 0 : -1 - this.prefetch = prefetch && !this.config.localOnly + this.prefetch = prefetch && this.config.wait this.prefetching = null } @@ -57,7 +57,7 @@ class RangeIterator { const key = v.keys.get(j) // skip non-local keys - if (this.config.localOnly && !key) continue + if (!this.config.wait && !key) continue const c = this.reverse ? b4a.compare(key.key, this.end) : b4a.compare(this.start, key.key) @@ -73,12 +73,12 @@ class RangeIterator { const j = this.reverse ? v.children.length - 1 - k : k const node = v.children.get(j) - if (!this.config.localOnly || node) { + if (this.config.wait || node) { this.stack.push({ node, offset: 0 }) } top.offset++ - if (this.config.localOnly && !node) break + if (!this.config.wait && !node) break } } @@ -86,7 +86,7 @@ class RangeIterator { let key = await this.nextKey() if (key === null) return null - if (!this.config.localOnly) { + if (this.config.wait) { return this.tree.finalizeKeyPointer(key, this.config) } @@ -116,7 +116,7 @@ class RangeIterator { if (k < v.children.length) { const j = this.reverse ? v.children.length - 1 - k : k const node = v.children.get(j) - if (!this.config.localOnly || node) { + if (this.config.wait || node) { this.stack.push({ node, offset: 0 }) } } @@ -127,7 +127,7 @@ class RangeIterator { const j = this.reverse ? v.keys.length - 1 - k : k const data = v.keys.get(j) - if (this.config.localOnly && !data) { + if (!this.config.wait && !data) { this.stack.push(top) continue } diff --git a/lib/session-config.js b/lib/session-config.js index 3d5510f..b763ffa 100644 --- a/lib/session-config.js +++ b/lib/session-config.js @@ -1,39 +1,34 @@ class SessionConfig { - constructor(activeRequests, timeout, wait, trace, localOnly = false) { + constructor(activeRequests, timeout, wait, trace) { this.activeRequests = activeRequests this.timeout = timeout this.wait = wait this.trace = trace - this.localOnly = localOnly } - sub(activeRequests, timeout, wait, trace, localOnly) { + sub(activeRequests, timeout, wait, trace) { if ( this.activeRequests === activeRequests && this.timeout === timeout && this.wait === wait && - this.trace === trace && - this.localOnly === localOnly + this.trace === trace ) { return this } - return new SessionConfig(activeRequests, timeout, wait, trace, localOnly) + return new SessionConfig(activeRequests, timeout, wait, trace) } options(opts) { if (!opts) return this - let { + const { activeRequests = this.activeRequests, timeout = this.timeout, wait = this.wait, - trace = this.trace, - localOnly = this.localOnly + trace = this.trace } = opts - if (localOnly) wait = false - - return this.sub(activeRequests, timeout, wait, trace, localOnly) + return this.sub(activeRequests, timeout, wait, trace) } detach(opts) { diff --git a/test/basic.js b/test/basic.js index 814e83b..9fae6ee 100644 --- a/test/basic.js +++ b/test/basic.js @@ -565,9 +565,7 @@ test('basic seq, offset and core', async function (t) { t.not(b.seq, c.seq) }) -test('throws hypercore error if block not available', async function (t) { - t.plan(1) - +test('get returns null if block not available and wait is false', async function (t) { const db = await create(t) await db.ready() @@ -582,12 +580,7 @@ test('throws hypercore error if block not available', async function (t) { await new Promise((resolve) => setTimeout(resolve, 100)) - try { - await db2.get(b4a.from('b'), { wait: false }) - t.fail('should have failed') - } catch (error) { - t.is(error.code, 'BLOCK_NOT_AVAILABLE') - } + t.alike(await db2.get(b4a.from('b'), { wait: false }), null) }) test('lock to avoid building concurrent batches', async function (t) { @@ -946,7 +939,7 @@ test('autoUpdate doesnt lose data', async function (t) { t.alike((await db.get(b4a.from('2'))).value, b4a.from('2')) }) -test('createReadStream localOnly', async function (t) { +test('createReadStream wait false', async function (t) { const db = await create(t) await db.ready() @@ -981,10 +974,9 @@ test('createReadStream localOnly', async function (t) { t.ok(await db2.core.has(3)) const actual = [] - for await (const data of db2.createReadStream({ localOnly: true })) { + for await (const data of db2.createReadStream({ wait: false })) { actual.push(data.key) } - // local view of the tree ( at block 3 height ) - t.alike(actual, [b4a.from('0'), b4a.from('1'), b4a.from('2'), b4a.from('3')]) + t.alike(actual, [b4a.from('1'), b4a.from('3')]) }) From f45a511ecaf16477c238b8a48ddf3562f40e5938 Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Mon, 29 Jun 2026 16:21:58 -0400 Subject: [PATCH 5/9] Update basic.js --- test/basic.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/basic.js b/test/basic.js index 9fae6ee..7e7b66f 100644 --- a/test/basic.js +++ b/test/basic.js @@ -565,6 +565,31 @@ test('basic seq, offset and core', async function (t) { t.not(b.seq, c.seq) }) +test('throws hypercore error if block not available', async function (t) { + t.plan(1) + + const db = await create(t) + await db.ready() + + const db2 = await create(t, { key: db.core.key, autoUpdate: true }) + await db2.ready() + + replicate(t, db, db2) + + const w = db.write() + w.tryPut(b4a.from('a'), b4a.alloc(32)) + await w.flush() + + await new Promise((resolve) => setTimeout(resolve, 100)) + + try { + await db2.get(b4a.from('b'), { wait: false }) + t.fail('should have failed') + } catch (error) { + t.is(error.code, 'BLOCK_NOT_AVAILABLE') + } +}) + test('get returns null if block not available and wait is false', async function (t) { const db = await create(t) await db.ready() From 1e7f46f37ead01161e4ae5a06bcf58c8c7ee5a97 Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Tue, 30 Jun 2026 13:21:53 -0400 Subject: [PATCH 6/9] fix --- lib/context.js | 10 +++++++++- test/basic.js | 12 ++++++++---- test/helpers/index.js | 4 +++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/context.js b/lib/context.js index 522fb79..1179a79 100644 --- a/lib/context.js +++ b/lib/context.js @@ -169,7 +169,15 @@ class CoreContext { } const hc = this.getCore(core) - const buffer = await hc.get(seq, config) + let buffer + + try { + buffer = await hc.get(seq, config) + } catch (err) { + if (err.code === 'REQUEST_TIMEOUT') throw BLOCK_NOT_AVAILABLE() + throw err + } + if (buffer === null) { if (!config.wait) return null throw BLOCK_NOT_AVAILABLE() diff --git a/test/basic.js b/test/basic.js index 7e7b66f..b6ec06e 100644 --- a/test/basic.js +++ b/test/basic.js @@ -571,19 +571,23 @@ test('throws hypercore error if block not available', async function (t) { const db = await create(t) await db.ready() - const db2 = await create(t, { key: db.core.key, autoUpdate: true }) + const db2 = await create(t, { key: db.core.key, autoUpdate: false }) await db2.ready() - replicate(t, db, db2) + const { s1, s2 } = replicate(t, db, db2) const w = db.write() w.tryPut(b4a.from('a'), b4a.alloc(32)) await w.flush() - await new Promise((resolve) => setTimeout(resolve, 100)) + // simulating inability to fetch the block + s1.destroy() + s2.destroy() + db2.move({ length: 1 }) + db2.cache.empty() try { - await db2.get(b4a.from('b'), { wait: false }) + await db2.get(b4a.from('a'), { wait: true, timeout: 500 }) t.fail('should have failed') } catch (error) { t.is(error.code, 'BLOCK_NOT_AVAILABLE') diff --git a/test/helpers/index.js b/test/helpers/index.js index 728a16f..fcbf9bf 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -5,7 +5,7 @@ exports.create = create exports.createMultiple = createMultiple exports.replicate = replicate -async function replicate(t, a, b) { +function replicate(t, a, b) { const s1 = a.replicate(true) const s2 = b.replicate(false) @@ -23,6 +23,8 @@ async function replicate(t, a, b) { await closed1 await closed2 }) + + return { s1, s2 } } async function create(t, opts) { From e17f17207e738162cec792817cf89b34f380885d Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Tue, 30 Jun 2026 14:14:37 -0400 Subject: [PATCH 7/9] remove block not available --- lib/context.js | 15 +++------------ test/basic.js | 29 ----------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/lib/context.js b/lib/context.js index 1179a79..f96ee28 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,6 +1,5 @@ const b4a = require('b4a') const ScopeLock = require('scope-lock') -const { BLOCK_NOT_AVAILABLE } = require('hypercore-errors') const { TreeNodePointer } = require('./tree.js') const { decodeBlock, TYPE_LATEST } = require('./encoding.js') @@ -169,18 +168,10 @@ class CoreContext { } const hc = this.getCore(core) - let buffer + const buffer = await hc.get(seq, config) - try { - buffer = await hc.get(seq, config) - } catch (err) { - if (err.code === 'REQUEST_TIMEOUT') throw BLOCK_NOT_AVAILABLE() - throw err - } - - if (buffer === null) { - if (!config.wait) return null - throw BLOCK_NOT_AVAILABLE() + if (buffer === null && !config.wait) { + return null } if (config.trace !== null) config.trace(core, seq) diff --git a/test/basic.js b/test/basic.js index b6ec06e..9fae6ee 100644 --- a/test/basic.js +++ b/test/basic.js @@ -565,35 +565,6 @@ test('basic seq, offset and core', async function (t) { t.not(b.seq, c.seq) }) -test('throws hypercore error if block not available', async function (t) { - t.plan(1) - - const db = await create(t) - await db.ready() - - const db2 = await create(t, { key: db.core.key, autoUpdate: false }) - await db2.ready() - - const { s1, s2 } = replicate(t, db, db2) - - const w = db.write() - w.tryPut(b4a.from('a'), b4a.alloc(32)) - await w.flush() - - // simulating inability to fetch the block - s1.destroy() - s2.destroy() - db2.move({ length: 1 }) - db2.cache.empty() - - try { - await db2.get(b4a.from('a'), { wait: true, timeout: 500 }) - t.fail('should have failed') - } catch (error) { - t.is(error.code, 'BLOCK_NOT_AVAILABLE') - } -}) - test('get returns null if block not available and wait is false', async function (t) { const db = await create(t) await db.ready() From 7b339fd8a40feeb0f30fb73e069d60bba6ce7ccd Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Tue, 30 Jun 2026 14:28:17 -0400 Subject: [PATCH 8/9] Update index.js --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 1ca84c5..4aa8e7c 100644 --- a/index.js +++ b/index.js @@ -220,8 +220,10 @@ class Hyperbee extends EventEmitter { } async inflate(ptr, config) { - const value = await inflate(ptr, config) - if (!value) return null + if (!ptr.value) { + const value = await inflate(ptr, config) + if (!value) return null + } this.bump(ptr) return ptr.value From 080f506fa8a78bf83f9563753e815ee8ee0a81ec Mon Sep 17 00:00:00 2001 From: Daniel Christopher Date: Tue, 30 Jun 2026 14:43:44 -0400 Subject: [PATCH 9/9] Update index.js --- test/helpers/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/helpers/index.js b/test/helpers/index.js index fcbf9bf..728a16f 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -5,7 +5,7 @@ exports.create = create exports.createMultiple = createMultiple exports.replicate = replicate -function replicate(t, a, b) { +async function replicate(t, a, b) { const s1 = a.replicate(true) const s2 = b.replicate(false) @@ -23,8 +23,6 @@ function replicate(t, a, b) { await closed1 await closed2 }) - - return { s1, s2 } } async function create(t, opts) {