OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-runtime commits / 908b033

installTree: single self-contained materialize (content-verify + skipPath) — match pgh
pgh-runtime's installTree manually recursed into subtrees, materializing each as its own store tree — redundant (duplicate per-subtree dirs) and unable to honor skipPath. Replace with pgh's tree-install.js logic: hasTree short-circuit → fetch → content-address verify (EHASHMISMATCH so a bad peer triggers a retry) → ONE store.materialize that inlines subtrees via fetchTree + honors skipPath. Closes the last installViaRegistry-parity gap (skipPath, threaded through installRelease). Updated the "walks a nested tree" test to assert the correct single-materialize behavior (subtree inlined, not a separate store tree). Tests: 16/16 pgh-runtime + 7/7 pgh integration. Real-network proof (createRuntime → hyperident) still produces an identical complete install. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
908b0338d7b995524fc7c4cbd50faeeed48c82b6
by plankatron <fleeky@gmail.com> · 2026-06-21T01:58:10.000Z

Changed files (3) +44 -36

index.js +37 -32
……@@ -43,7 +43,7 @@
4343
4444import EventEmitter from '#node:events'
4545
46import { KIND_DIR } from 'hyper-overlay/tree'
46import { computeTreeHash } from 'hyper-overlay/tree'
4747
4848const DEFAULT_INTERVAL = 30 * 60 * 1000 // 30 min
4949
……@@ -191,41 +191,46 @@ export class PghRuntime extends EventEmitter {
191191 */
192192export async function installTree (store, rootHash, fetcher, opts = {}) {
193193 store.init()
194 let materialized = 0
195 let skipped = 0
196 const seen = new Set()
197
198 async function visit (hash) {
199 if (seen.has(hash)) return
200 seen.add(hash)
201194
202 if (store.hasTree(hash)) {
203 skipped++
204 return
205 }
206
207 const tree = await fetcher.getTree(hash)
208 if (!tree || !Array.isArray(tree.entries)) {
209 throw new Error('fetcher returned invalid tree for ' + hash)
210 }
195 if (store.hasTree(rootHash)) {
196 return { materialized: 0, skipped: 1 }
197 }
211198
212 for (const e of tree.entries) {
213 if (e.kind === KIND_DIR) await visit(e.childHash)
214 }
199 const tree = await fetcher.getTree(rootHash)
200 if (!tree || !Array.isArray(tree.entries)) {
201 throw new Error('fetcher returned invalid tree for ' + rootHash)
202 }
215203
216 await store.materialize(hash, tree.entries, {
217 fetchContent: fetcher.getContent.bind(fetcher),
218 fetchTree: async (h) => {
219 const t = await fetcher.getTree(h)
220 return t.entries
221 },
222 source: opts.source
223 })
224 materialized++
204 // Content-address verification: entries MUST hash to rootHash. A P2P peer
205 // can answer with empty/wrong entries to signal "I don't have this tree";
206 // without this check that silently materializes an empty/partial dir. Let
207 // the caller retry on another peer (EHASHMISMATCH). Matches pgh's installTree.
208 const actualRootHash = computeTreeHash(tree.entries)
209 if (actualRootHash !== rootHash) {
210 throw Object.assign(
211 new Error('root tree-hash mismatch: expected ' + rootHash.slice(0, 12) + '…, got ' + actualRootHash.slice(0, 12) + '… (peer may not have this tree)'),
212 { code: 'EHASHMISMATCH', expected: rootHash, actual: actualRootHash }
213 )
225214 }
226215
227 await visit(rootHash)
228 return { materialized, skipped }
216 // Single self-contained materialize — store.materialize recursively inlines
217 // subtree content via fetchTree/fetchContent. Subtree hashes do NOT get their
218 // own dependencies/<subhash>/ dir (that would duplicate every file N times
219 // across levels); dedup happens at the top-level tree layer. `skipPath` skips
220 // foreign subtrees (e.g. by-arch/<other-host>/). Mirrors pgh's tree-install.js
221 // — the manual subtree recursion this replaced created redundant store dirs
222 // and could not honor skipPath.
223 await store.materialize(rootHash, tree.entries, {
224 fetchContent: fetcher.getContent.bind(fetcher),
225 fetchTree: async (h) => {
226 const t = await fetcher.getTree(h)
227 return t.entries
228 },
229 source: opts.source,
230 skipPath: opts.skipPath
231 })
232
233 return { materialized: 1, skipped: 0 }
229234}
230235
231236/**
……@@ -279,7 +284,7 @@ export async function installRelease (store, release, fetcher, opts = {}) {
279284 let materialized = 0
280285 let skipped = 0
281286 for (const hash of closureHashes) {
282 const r = await installTree(store, hash, fetcher, { source })
287 const r = await installTree(store, hash, fetcher, { source, skipPath: opts.skipPath })
283288 materialized += r.materialized
284289 skipped += r.skipped
285290 }
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh-runtime",
3 "version": "0.1.3",
3 "version": "0.1.4",
44 "description": "Per-app OTA updater for pgh-installed apps — polls a release source, materializes new trees into the pgh store",
55 "type": "module",
66 "main": "index.js",
test/runtime.test.js +6 -3
……@@ -335,9 +335,12 @@ test('installTree — direct call walks a nested tree', async (t) => {
335335
336336 const store = makeMemoryStore()
337337 const result = await installTree(store, rootHash, customFetcher)
338 t.is(result.materialized, 2, 'root + leaf')
339 t.ok(store.hasTree(rootHash))
340 t.ok(store.hasTree(leafHash))
338 // Single self-contained materialize: the root tree is materialized once and
339 // store.materialize inlines the 'lib' subtree via fetchTree — the subtree
340 // does NOT become its own store tree (no redundant per-subtree dir).
341 t.is(result.materialized, 1, 'one self-contained materialize of the root')
342 t.ok(store.hasTree(rootHash), 'root tree present')
343 t.absent(store.hasTree(leafHash), 'subtree inlined into root, not a separate store tree')
341344})
342345
343346test('installTree — skips already-present trees', async (t) => {