// Persistent DeployServer fileIndex (root-cause fix for expensive cold-boot
// re-scan + fragile tree serving across restarts).
//
// hyper-deploy's DeployServer defaults to a fresh os.tmpdir() corestore on
// every open, so it re-hashes the ENTIRE served dir on every boot and only
// serves trees indexed by the current process. pgh-sidecar now passes a
// PERSISTENT corestore (opts.corestore) so the fileIndex survives restarts.
//
// This proves the mechanism the fix relies on:
// 1. After a first open, trees are registered (getTree returns entries).
// 2. Reopening with the SAME persistent corestore keeps trees registered
// (durable across "restart") AND reuses cached hashes instead of
// re-hashing (scan-cache reports hits, not all-misses).
// Contrast test: an EPHEMERAL (fresh) corestore re-hashes everything (all
// misses) on the second open — the pathology the fix removes.
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Corestore from 'corestore'
import { DeployServer } from 'hyper-deploy'
let n = 0
function tmpDir (tag) {
const d = path.join(os.tmpdir(), 'pgh-deployidx-' + tag + '-' + (n++) + '-' + Math.floor(os.uptime() * 1000))
fs.mkdirSync(d, { recursive: true })
return d
}
// Build a small content-addressed-ish served dir: a couple of "tree" subdirs
// with a few files each, enough that a re-hash is observable as misses.
function makeContentDir () {
const dir = tmpDir('content')
for (const treeName of ['aaaa', 'bbbb']) {
const td = path.join(dir, treeName)
fs.mkdirSync(path.join(td, 'lib'), { recursive: true })
fs.writeFileSync(path.join(td, 'index.js'), '// ' + treeName + '\nmodule.exports = ' + JSON.stringify(treeName) + '\n')
fs.writeFileSync(path.join(td, 'package.json'), JSON.stringify({ name: treeName, version: '1.0.0' }) + '\n')
fs.writeFileSync(path.join(td, 'lib', 'a.js'), 'exports.a = 1\n'.repeat(50))
}
return dir
}
async function openServer (contentDir, storeDir) {
const store = new Corestore(storeDir)
const stats = { hits: 0, misses: 0 }
const srv = new DeployServer(contentDir, { corestore: store, swarm: false, topic: Buffer.alloc(32, 1) })
srv.on('scan-cache-stats', (s) => { stats.hits = s.hits; stats.misses = s.misses })
await srv.ready()
return { srv, store, stats }
}
test('persistent store: trees survive a restart and are not re-hashed', async (t) => {
t.timeout(60000)
const contentDir = makeContentDir()
const storeDir = tmpDir('persist-store')
// --- First open (cold): indexes + registers trees ---
const a = await openServer(contentDir, storeDir)
t.ok(a.srv.manifest, 'first open produced a manifest')
const rootHash = a.srv.manifest.rootTreeHash
t.ok(rootHash, 'root tree hash was published')
const treeA = await a.srv._fileIndex.getTree(rootHash)
t.ok(treeA && treeA.entries && treeA.entries.length > 0, 'getTree(root) returns entries after first open')
t.is(a.stats.hits, 0, 'first (cold) open: no cache hits — everything hashed')
t.ok(a.stats.misses > 0, 'first open hashed files (misses > 0)')
await a.srv.close()
await a.store.close() // DeployServer doesn't own a passed-in store — release the lock (this is why the sidecar's close() closes it explicitly)
// --- Reopen with the SAME persistent store (simulates a daemon restart) ---
const b = await openServer(contentDir, storeDir)
const treeAafter = await b.srv._fileIndex.getTree(rootHash)
t.ok(treeAafter && treeAafter.entries && treeAafter.entries.length > 0,
'getTree(root) STILL returns entries after restart (durable serving)')
t.ok(b.stats.hits > 0,
'restart with persistent store REUSED cached hashes (hits > 0) — no full re-hash')
await b.srv.close()
await b.store.close()
t.teardown(async () => {
try { fs.rmSync(contentDir, { recursive: true, force: true }) } catch {}
try { fs.rmSync(storeDir, { recursive: true, force: true }) } catch {}
})
})
test('contrast: ephemeral (fresh) store re-hashes everything on second open', async (t) => {
t.timeout(60000)
const contentDir = makeContentDir()
const a = await openServer(contentDir, tmpDir('ephem-1'))
t.ok(a.stats.misses > 0, 'first open hashed files')
await a.srv.close()
await a.store.close()
// Different store dir = the old ephemeral behavior.
const b = await openServer(contentDir, tmpDir('ephem-2'))
t.is(b.stats.hits, 0, 'fresh store: zero cache hits (re-hashes everything) — the pathology the fix removes')
t.ok(b.stats.misses > 0, 'fresh store re-hashed all files')
await b.srv.close()
await b.store.close()
t.teardown(async () => { try { fs.rmSync(contentDir, { recursive: true, force: true }) } catch {} })
})