OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / bff5866

port(0.3.0): adopt hyper-overlay-v3 chunker.js + bump 0.2.14->0.3.0
chunk() output verified byte-identical to 0.2.14 across tiers 0/1/2 (small, 11MB .node, 390MB .pck) -> no content-hash change, store stays compatible. Brings block-read + streaming-hasher snapshot additions needed by transfer.js. Increment 1 of the fork-hardening port. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bff5866831278b8d3d1d0a721a70c395f4268437
by plankatron <fleeky@gmail.com> · 2026-07-11T02:24:21.000Z

Changed files (2) +43 -35

lib/chunker.js +42 -34
……@@ -202,12 +202,19 @@ const LEAF_TYPE = Buffer.from([0])
202202 */
203203export function createStreamingHasher (opts = {}) {
204204 const state = Buffer.alloc(sodium.crypto_generichash_STATEBYTES)
205 sodium.crypto_generichash_init(state, null, 32)
206205 let bytes = 0
207 const knownSize = (opts && typeof opts.size === 'number') ? opts.size : null
208 if (knownSize !== null) {
209 sodium.crypto_generichash_update(state, LEAF_TYPE)
210 sodium.crypto_generichash_update(state, c.encode(c.uint64, knownSize))
206 if (opts && opts.restore) {
207 // The saved state already includes the init + leaf/size prefix + every byte
208 // consumed so far, so do not re-init.
209 opts.restore.state.copy(state)
210 bytes = opts.restore.bytes
211 } else {
212 sodium.crypto_generichash_init(state, null, 32)
213 const knownSize = (opts && typeof opts.size === 'number') ? opts.size : null
214 if (knownSize !== null) {
215 sodium.crypto_generichash_update(state, LEAF_TYPE)
216 sodium.crypto_generichash_update(state, c.encode(c.uint64, knownSize))
217 }
211218 }
212219 return {
213220 update (buf) {
……@@ -219,6 +226,7 @@ export function createStreamingHasher (opts = {}) {
219226 sodium.crypto_generichash_final(state, out)
220227 return out.toString('hex')
221228 },
229 snapshot () { return { state: Buffer.from(state), bytes } },
222230 get bytes () { return bytes }
223231 }
224232}
……@@ -250,6 +258,15 @@ function findCutPoint (data, params, masks) {
250258 return maxLen
251259}
252260
261// Opt-in memcpy accounting for perf validation. Zero cost when off (one
262// boolean check at the increment sites). Module-level singletons — enabled
263// only by tests + bench/prepare.js, never in library code; NOT
264// concurrency-safe, so do not enable it in production.
265let STATS_ON = false
266export const chunkStats = { concatBytes: 0, copyBytes: 0, blocks: 0, chunks: 0 }
267export function resetChunkStats () { chunkStats.concatBytes = 0; chunkStats.copyBytes = 0; chunkStats.blocks = 0; chunkStats.chunks = 0 }
268export function setChunkStats (on) { STATS_ON = !!on; if (on) resetChunkStats() }
269
253270/**
254271 * Stream-friendly FastCDC chunker. Consumes an AsyncIterable<Buffer>
255272 * (e.g. fs.createReadStream) and yields chunks one at a time. Memory
……@@ -259,41 +276,45 @@ function findCutPoint (data, params, masks) {
259276 *
260277 * Each yielded chunk has the same shape as `chunk()` entries:
261278 * { hash, offset, length, data }
262 * but `data` is a fresh `Buffer.from(slice)` detached from the input
263 * stream's pool, so callers can store/forward it without pinning the
264 * reader's buffers.
279 * With the default `copy: true`, `data` is a fresh `Buffer.from(slice)`
280 * detached from the input stream's pool, so callers can store/forward it.
281 * With `copy: false`, `data` is a view backed by the reader's memory —
282 * valid only until the next iteration; the caller MUST consume it
283 * synchronously and not retain it across an await.
265284 *
266285 * Cut-point determinism: same content + same tier → identical chunk
267 * boundaries as `chunk()`. Peers using either function on the same
268 * file produce matching chunk hashes.
286 * boundaries as `chunk()`, regardless of input block size. Peers using
287 * either function on the same file produce matching chunk hashes.
269288 *
270289 * @param {AsyncIterable<Buffer>} readable
271 * @param {{ tier?: number }} [opts] tier index (0–3). Callers with a
272 * known file size should pass `selectTier(size)`. Defaults to tier 0.
290 * @param {{ tier?: number, copy?: boolean }} [opts] tier index (0–3); copy
291 * defaults true. Callers with a known file size should pass `selectTier(size)`.
273292 */
274293export async function * chunkStream (readable, opts = {}) {
275294 const tier = (opts && typeof opts.tier === 'number') ? opts.tier : 0
295 const copy = opts.copy !== false // false → yield views (see JSDoc above)
276296 const params = TIERS[tier] || TIERS[0]
277297 const masks = TIER_MASKS[tier] || TIER_MASKS[0]
278298
279299 let pending = Buffer.alloc(0)
280300 let absoluteOffset = 0
281301
302 // Single emit point for the stats accounting + copy:false decision.
303 const emit = (slice) => {
304 if (STATS_ON) { chunkStats.chunks++; if (copy) chunkStats.copyBytes += slice.length }
305 return { hash: hashChunk(slice), offset: absoluteOffset, length: slice.length, data: copy ? Buffer.from(slice) : slice }
306 }
307
282308 for await (const inChunk of readable) {
283309 const buf = Buffer.isBuffer(inChunk) ? inChunk : Buffer.from(inChunk)
310 if (STATS_ON) { chunkStats.blocks++; if (pending.length !== 0) chunkStats.concatBytes += pending.length + buf.length }
284311 pending = pending.length === 0 ? buf : Buffer.concat([pending, buf])
285312
286313 // While we have at least maxSize ahead, we can definitively find
287314 // the next cut point inside `pending` (the cut is bounded by maxSize).
288315 while (pending.length >= params.maxSize) {
289316 const cutLen = findCutPoint(pending, params, masks)
290 const slice = pending.subarray(0, cutLen)
291 yield {
292 hash: hashChunk(slice),
293 offset: absoluteOffset,
294 length: cutLen,
295 data: Buffer.from(slice)
296 }
317 yield emit(pending.subarray(0, cutLen))
297318 absoluteOffset += cutLen
298319 pending = pending.subarray(cutLen)
299320 }
……@@ -303,23 +324,10 @@ export async function * chunkStream (readable, opts = {}) {
303324 // the buffer-mode `chunk()`.
304325 while (pending.length > params.minSize) {
305326 const cutLen = findCutPoint(pending, params, masks)
306 const slice = pending.subarray(0, cutLen)
307 yield {
308 hash: hashChunk(slice),
309 offset: absoluteOffset,
310 length: cutLen,
311 data: Buffer.from(slice)
312 }
327 yield emit(pending.subarray(0, cutLen))
313328 absoluteOffset += cutLen
314329 pending = pending.subarray(cutLen)
315330 }
316331
317 if (pending.length > 0) {
318 yield {
319 hash: hashChunk(pending),
320 offset: absoluteOffset,
321 length: pending.length,
322 data: Buffer.from(pending)
323 }
324 }
332 if (pending.length > 0) yield emit(pending)
325333}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-overlay",
3 "version": "0.2.14",
3 "version": "0.3.0",
44 "description": "Content-addressed union filesystem — P2P file distribution with FastCDC chunking and zero duplication",
55 "type": "module",
66 "main": "index.js",