| … | … | @@ -39,6 +39,12 @@ export class HyperOverlay extends ReadyResource { |
| 39 | 39 | this._registry = null |
| 40 | 40 | this._protocol = null |
| 41 | 41 | this._namespaceStore = corestore.namespace(this._namespace) |
| 42 | // Optional content-addressed spool for peer-fetched chunks. When set, |
| 43 | // fetched bytes are written here and registered as `local` pointers |
| 44 | // instead of base64-inflated inline `cache` sources. See |
| 45 | // _cacheFetchedChunk(). Null = legacy inline-cache behavior. |
| 46 | this._chunkSpoolDir = opts.chunkSpoolDir || null |
| 47 | this._spoolSeq = 0 |
| 42 | 48 | } |
| 43 | 49 | |
| 44 | 50 | async _open () { |
| … | … | @@ -329,11 +335,7 @@ export class HyperOverlay extends ReadyResource { |
| 329 | 335 | data = await this._protocol.requestChunk(fileInfo.manifestHash, i) |
| 330 | 336 | if (data) { |
| 331 | 337 | // Cache the fetched chunk locally |
| 332 | | await this._registry.putChunk(c.hash, { |
| 333 | | size: data.length, |
| 334 | | type: 'cache', |
| 335 | | data: data.toString('base64') |
| 336 | | }) |
| 338 | await this._cacheFetchedChunk(c.hash, data) |
| 337 | 339 | } |
| 338 | 340 | } catch { |
| 339 | 341 | // Peer fetch failed, continue |
| … | … | @@ -483,7 +485,7 @@ export class HyperOverlay extends ReadyResource { |
| 483 | 485 | try { |
| 484 | 486 | data = await this._protocol.requestChunk(fileInfo.manifestHash, i) |
| 485 | 487 | if (data) { |
| 486 | | await this._registry.putChunk(c.hash, { size: data.length, type: 'cache', data: data.toString('base64') }) |
| 488 | await this._cacheFetchedChunk(c.hash, data) |
| 487 | 489 | } |
| 488 | 490 | } catch {} |
| 489 | 491 | } |
| … | … | @@ -539,7 +541,7 @@ export class HyperOverlay extends ReadyResource { |
| 539 | 541 | if (!data && overlay._protocol && fileInfo.manifestHash) { |
| 540 | 542 | data = await overlay._protocol.requestChunk(fileInfo.manifestHash, idx) |
| 541 | 543 | if (data) { |
| 542 | | await overlay._registry.putChunk(c.hash, { size: data.length, type: 'cache', data: data.toString('base64') }) |
| 544 | await overlay._cacheFetchedChunk(c.hash, data) |
| 543 | 545 | } |
| 544 | 546 | } |
| 545 | 547 | if (!data) return cb(new Error('Chunk unavailable: ' + c.hash)) |
| … | … | @@ -583,7 +585,7 @@ export class HyperOverlay extends ReadyResource { |
| 583 | 585 | const data = await this._protocol.requestChunk(fileInfo.manifestHash, i) |
| 584 | 586 | if (!data) throw new Error('Chunk unavailable from peers: ' + c.hash) |
| 585 | 587 | |
| 586 | | await this._registry.putChunk(c.hash, { size: data.length, type: 'cache', data: data.toString('base64') }) |
| 588 | await this._cacheFetchedChunk(c.hash, data) |
| 587 | 589 | fetched++ |
| 588 | 590 | |
| 589 | 591 | if (opts.onProgress) opts.onProgress({ fetched, total: chunkList.length }) |
| … | … | @@ -692,14 +694,44 @@ export class HyperOverlay extends ReadyResource { |
| 692 | 694 | if (!remote) return null |
| 693 | 695 | |
| 694 | 696 | // Cache the fetched bytes locally so subsequent calls are free |
| 695 | | await this._registry.putChunk(hash, { |
| 696 | | size: remote.length, |
| 697 | | type: 'cache', |
| 698 | | data: remote.toString('base64') |
| 699 | | }) |
| 697 | await this._cacheFetchedChunk(hash, remote) |
| 700 | 698 | |
| 701 | 699 | return remote |
| 702 | 700 | } |
| 701 | |
| 702 | /** |
| 703 | * Persist a chunk just fetched from a peer. |
| 704 | * |
| 705 | * With a configured spool dir (`opts.chunkSpoolDir`) the raw bytes are |
| 706 | * written to a content-addressed file `<spoolDir>/<hash>` and registered |
| 707 | * as a `local` pointer — no base64 +33% tax, and ChunkRegistry's |
| 708 | * cross-mode dedup then prefers it over any inline copy. Without a spool |
| 709 | * dir (default) it falls back to the legacy inline base64 `cache` source, |
| 710 | * so behavior is unchanged for callers that don't opt in. Any spool I/O |
| 711 | * error also falls back to inline cache, so a fetch never fails just |
| 712 | * because the spool is unwritable. |
| 713 | * |
| 714 | * @param {string} hash - blake2b chunk hash (hex) |
| 715 | * @param {Buffer} data - the fetched chunk bytes |
| 716 | */ |
| 717 | async _cacheFetchedChunk (hash, data) { |
| 718 | if (this._chunkSpoolDir) { |
| 719 | try { |
| 720 | const dest = path.join(this._chunkSpoolDir, hash) |
| 721 | if (!fs.existsSync(dest)) { |
| 722 | await fs.promises.mkdir(this._chunkSpoolDir, { recursive: true }) |
| 723 | const tmp = dest + '.tmp-' + (++this._spoolSeq) |
| 724 | await fs.promises.writeFile(tmp, data) |
| 725 | await fs.promises.rename(tmp, dest) |
| 726 | } |
| 727 | await this._registry.putChunk(hash, { size: data.length, type: 'local', path: dest, offset: 0 }) |
| 728 | return |
| 729 | } catch { |
| 730 | // fall through to inline cache on any spool error |
| 731 | } |
| 732 | } |
| 733 | await this._registry.putChunk(hash, { size: data.length, type: 'cache', data: data.toString('base64') }) |
| 734 | } |
| 703 | 735 | } |
| 704 | 736 | |
| 705 | 737 | // Re-exports (v1 — kept for backward compatibility) |