OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy / test / overlay-map-cache.test.js

86 lines · javascript ref:
/**
 * serveOnConnection must not rebuild the overlay lookup maps per connection.
 *
 * It built contentHashPaths + filePaths from scratch on EVERY inbound
 * connection. Measured on tabertron's tree store (39,484 files / 2,977 trees)
 * that is 3.8 MB of garbage and ~79,000 map insertions per connection, for a
 * result identical every time until the manifest changes.
 *
 * It is also largely redundant: _scanIncremental already pushes new entries
 * into the live maps so peers connected BEFORE a rescan can find new bytes
 * without reconnecting. The full rebuild only has to happen when the manifest
 * object is REPLACED (_scan / _scanBoot), not when it is appended to.
 *
 * Hence the cache key is manifest IDENTITY:
 *   - _scan / _scanBoot assign a new manifest object  -> rebuild
 *   - _scanIncremental pushes into the existing array -> reuse, and its own
 *     .set() calls keep the cached maps current
 */

import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import SecretStream from '@hyperswarm/secret-stream'

import { DeployServer } from '../index.js'

function mktemp (prefix) {
  const dir = path.join(os.tmpdir(), 'hyper-deploy-mapcache-' + prefix + '-' + crypto.randomBytes(6).toString('hex'))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

function pair () {
  const a = new SecretStream(true)
  const b = new SecretStream(false)
  a.rawStream.pipe(b.rawStream).pipe(a.rawStream)
  return { a, b }
}

test('overlay lookup maps are reused across connections, rebuilt on rescan', async (t) => {
  t.timeout(30000)

  const dir = mktemp('cache')
  fs.writeFileSync(path.join(dir, 'a.txt'), 'hello')

  const server = new DeployServer(dir)
  await server.ready()
  t.teardown(async () => {
    try { await server.close() } catch {}
    try { fs.rmSync(dir, { recursive: true, force: true }) } catch {}
  })

  const proto = server._overlayProtocol
  t.ok(proto, 'overlay protocol enabled')

  const c1 = pair()
  server._onConnection(c1.a)
  const maps1 = { hashes: proto._contentHashPaths, paths: proto._filePaths }
  t.ok(maps1.hashes instanceof Map, 'first connection populates the maps')
  t.ok(maps1.hashes.size > 0, 'maps are non-empty')

  const c2 = pair()
  server._onConnection(c2.a)

  t.is(proto._contentHashPaths, maps1.hashes,
    'second connection REUSES the same contentHashPaths instance — no rebuild')
  t.is(proto._filePaths, maps1.paths,
    'second connection REUSES the same filePaths instance — no rebuild')

  // A full rescan replaces the manifest object, so the cache must drop.
  fs.writeFileSync(path.join(dir, 'b.txt'), 'second file')
  await server.rescan()

  const c3 = pair()
  server._onConnection(c3.a)

  t.not(proto._contentHashPaths, maps1.hashes,
    'a full rescan (new manifest object) forces a rebuild')
  t.is(proto._filePaths.size, server.manifest.files.length,
    'rebuilt maps cover every manifest entry')

  for (const c of [c1, c2, c3]) { c.a.destroy(); c.b.destroy() }
})