OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-sidecar commits / ddb479a

0.2.51: releases-summary — list releases without the dep closure
`pgh releases list pear-git-v3` never returns. Measured 2026-08-07: every registry entry embeds its full dep closure, and ONE pear-git-v3 entry is ~120 KB (285 deps, each with a per-arch prebuild hash map). Across every published version releases-list has to serialise tens of MB over the UDS, so the CLI times out at 60s and nothing can be built on it. releases-summary returns identity + provenance only -- name, version, treeHash, upgrade, publishedAt, publisherDevicePubkey -- plus depCount so a caller can still show closure size without paying for it. releases-list is left untouched; install/resolve paths genuinely need deps. Prompted by pear-git-v3 needing to render registry rows on the Releases tab. Its own tree-registry-list read the store directly and always failed with "File descriptor could not be locked" -- this sidecar holds that RocksDB lock for its whole lifetime -- so that handler is being repointed here. Test publishes a 200-dep closure and asserts the summary drops deps, keeps depCount, and is >20x smaller than the same entry from releases-list. Pre-existing and untouched: test 1 ("start creates identity + store dirs") fails on this machine with and without this change -- it asserts a raw identity.json that a hyperident-vault host does not write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ddb479ab7f206031bac5e452ddeb836c820a868b
by plankatron <fleeky@gmail.com> · 2026-08-07T23:59:06.000Z

Changed files (3) +96 -1

lib/index.js +31 -0
……@@ -1455,6 +1455,37 @@ export class Sidecar {
14551455 return [...own, ...fromReplicas]
14561456 })
14571457
1458 // Slim projection of releases-list, for callers that only DISPLAY releases.
1459 //
1460 // Every registry entry embeds its full dep closure — measured 2026-08-07,
1461 // ONE pear-git-v3 entry is ~120 KB (285 deps, each with a per-arch prebuild
1462 // hash map). So `releases-list` has to serialise that once per version:
1463 // `pgh releases list pear-git-v3` never returns, and pear-git-v3's
1464 // `tree-registry-list` (which reads the store directly) could not be used to
1465 // back a UI even after its lock conflict was fixed.
1466 //
1467 // `releases-list` is deliberately left intact — install/resolve paths need
1468 // `deps`. This one carries identity + provenance only, plus depCount so a
1469 // caller can still show closure size without paying for it.
1470 svc.handle('releases-summary', async (ctx, params) => {
1471 const filters = params || {}
1472 const slim = (e) => ({
1473 name: e.name,
1474 version: e.version,
1475 treeHash: e.treeHash,
1476 publishedAt: e.publishedAt,
1477 publisherDevicePubkey: e.publisherDevicePubkey,
1478 runtime: e.runtime || null,
1479 upgrade: e.upgrade || null,
1480 depCount: Array.isArray(e.deps) ? e.deps.length : 0
1481 })
1482 const out = (await this._registry.list(filters.name)).map(slim)
1483 for (const r of this._releaseReplicas) {
1484 try { out.push(...(await r.registry.list(filters.name)).map(slim)) } catch {}
1485 }
1486 return out
1487 })
1488
14581489 svc.handle('releases-latest', async (ctx, params) => {
14591490 if (!params || !params.name) throw new Error('releases-latest requires { name }')
14601491 let best = await this._registry.latest(params.name)
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh-sidecar",
3 "version": "0.2.50",
3 "version": "0.2.51",
44 "description": "Small optional user-level daemon for pgh \u2014 shared identity vault, OTA polling, device linking, bootstrap seed, NameRegistry host",
55 "type": "module",
66 "main": "lib/index.js",
test/sidecar.test.js +64 -0
……@@ -246,6 +246,70 @@ test('registry-info / publish-release / releases-list round-trip', async (t) =>
246246 await sidecar.stop()
247247})
248248
249test('releases-summary omits the dep closure', async (t) => {
250 // Every registry entry embeds its full dep closure. Measured on prod
251 // 2026-08-07, ONE pear-git-v3 entry is ~120 KB (285 deps x per-arch prebuild
252 // hash maps), so `releases-list` never returns across many versions —
253 // `pgh releases list pear-git-v3` times out at 60s. releases-summary exists so
254 // a caller that only DISPLAYS releases does not pay for the closure.
255 //
256 // PASS = summary carries identity + provenance + depCount, carries NO deps,
257 // and is dramatically smaller than the same entry from releases-list.
258 const { sidecar, sock } = await makeSidecar()
259 const client = await makeClient(sock)
260
261 // A closure big enough that "did we actually drop it" is unambiguous.
262 const deps = []
263 for (let i = 0; i < 200; i++) {
264 deps.push({
265 name: 'dep-' + i,
266 version: '1.0.0',
267 tree: 'f'.repeat(64),
268 prebuilds: {
269 'linux-x64': 'a'.repeat(64),
270 'linux-arm64': 'b'.repeat(64),
271 'darwin-arm64': 'c'.repeat(64)
272 }
273 })
274 }
275
276 await client.call('publish-release', {
277 entry: {
278 name: 'fat-app',
279 version: '2.0.0',
280 treeHash: 'deadbeef',
281 upgrade: 'pgh://reg/fat-app',
282 publisherDevicePubkey: 'pub123',
283 deps,
284 publishedAt: Date.now()
285 }
286 })
287
288 const summary = await client.call('releases-summary', {})
289 t.is(summary.length, 1, 'one entry')
290 const s = summary[0]
291 t.is(s.name, 'fat-app')
292 t.is(s.version, '2.0.0')
293 t.is(s.treeHash, 'deadbeef', 'treeHash retained — it is the install identity')
294 t.is(s.upgrade, 'pgh://reg/fat-app', 'upgrade URL retained')
295 t.is(s.publisherDevicePubkey, 'pub123', 'provenance retained')
296 t.is(s.depCount, 200, 'closure SIZE reported without the closure')
297 t.absent(s.deps, 'deps dropped — this is the whole point')
298
299 const full = await client.call('releases-list', {})
300 t.is(full[0].deps.length, 200, 'releases-list still carries deps for install paths')
301
302 const sBytes = JSON.stringify(summary).length
303 const fBytes = JSON.stringify(full).length
304 t.ok(sBytes * 20 < fBytes, 'summary is >20x smaller (' + sBytes + ' vs ' + fBytes + ' bytes)')
305
306 const filtered = await client.call('releases-summary', { name: 'nope' })
307 t.is(filtered.length, 0, 'name filter applies')
308
309 await client.close()
310 await sidecar.stop()
311})
312
249313test('seed-status reports enabled:false when enableSeed:false', async (t) => {
250314 const { sidecar, sock } = await makeSidecar()
251315 const client = await makeClient(sock)