OverviewFilesCommitsIssuesDocsReleasesPeers

pgh-sidecar commits / db1197e

0.2.36: NameRegistry read-only replicas + subscribe-name-registry
Companion to pgh 0.1.94. The sidecar now replicates OTHER publishers' name registries (not just its own single bootstrap), so a standalone peer can resolve bare app names claimed elsewhere: - nameRegistrySubscriptions (subscriptions.json nameRegistries[] + PGH_NAME_REGISTRIES) → _openNameReplica() opens a read-only warm replica per key on boot, joined on its discoveryKey. - resolve-name + list-names now query own registry THEN every replica (_resolveNameAcross / merge). - subscribe-name-registry handler: persists + opens a live replica with no restart; name-subscriptions lists them. - _close tears down name replicas. Mirrors the existing release-registry replica scheme exactly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
db1197e4fcfdbad29f491e35eac8a528c4b67c5b
by plankatron <fleeky@gmail.com> · 2026-06-17T01:13:16.000Z

Changed files (2) +120 -4

lib/index.js +119 -3
……@@ -37,7 +37,7 @@ import { TreeStore, DEFAULT_ROOT as PGH_STORE_ROOT } from 'pgh/store'
3737import { loadOrCreateIdentity } from 'pgh/identity'
3838import { ReleaseRegistry } from 'pgh/release-registry'
3939import { NameRegistry } from 'pgh/name-registry'
40import { readSubscriptions as readSubscriptionsFromDisk } from 'pgh/subscriptions'
40import { readSubscriptions as readSubscriptionsFromDisk, readNameSubscriptions, addNameSubscription } from 'pgh/subscriptions'
4141
4242import { OverlayManager } from './overlay-manager.js'
4343import { SyncEngine } from 'hyper-overlay/sync-engine'
……@@ -109,6 +109,15 @@ export class Sidecar {
109109 : []
110110 this.releaseRegistrySubscriptions = opts.releaseRegistrySubscriptions ||
111111 [...new Set([...fromConfig, ...fromEnv])]
112 // NameRegistry subscriptions — read-only replicas of OTHER publishers'
113 // name registries so bare app-name resolution works cross-publisher.
114 // Sourced from subscriptions.json `nameRegistries` + PGH_NAME_REGISTRIES.
115 const nameFromConfig = readNameSubscriptions()
116 const nameFromEnv = process.env.PGH_NAME_REGISTRIES
117 ? process.env.PGH_NAME_REGISTRIES.split(',').map(s => s.trim()).filter(Boolean)
118 : []
119 this.nameRegistrySubscriptions = opts.nameRegistrySubscriptions ||
120 [...new Set([...nameFromConfig, ...nameFromEnv])]
112121 this.passphrase = opts.passphrase || null
113122 // Seeding can be disabled in tests (no swarm / no DeployServer)
114123 this.enableSeed = opts.enableSeed !== false
……@@ -123,6 +132,7 @@ export class Sidecar {
123132 this._deployServer = null // hyper-deploy DeployServer (advertises trees)
124133 this._registrySwarm = null // Hyperswarm advertising the release-registry
125134 this._releaseReplicas = [] // [{ keyHex, store, registry, swarm }] — warm replicas
135 this._nameReplicas = [] // [{ keyHex, store, registry, swarm }] — read-only NameRegistry replicas
126136 // Running-app registry. Apps register with us on boot so `pgh app
127137 // resync|start|stop|restart|status` can find their UDS. Persisted
128138 // to disk so sidecar restarts don't lose the map — otherwise every
……@@ -289,6 +299,17 @@ export class Sidecar {
289299 this._nameRegistrySwarm.join(this._nameRegistry.discoveryKey, { server: true, client: true })
290300 await this._nameRegistrySwarm.flush()
291301
302 // Read-only replicas of OTHER publishers' name registries, so a
303 // standalone peer (own empty NameRegistry) can resolve bare app
304 // names claimed elsewhere. Mirrors the release-replica scheme below.
305 const ownNameKey = this._nameRegistry && this._nameRegistry.keyHex
306 for (const keyHex of this.nameRegistrySubscriptions) {
307 if (keyHex.length !== 64) continue
308 if (keyHex === ownNameKey) continue // own registry already queried first
309 const replica = await this._openNameReplica(keyHex)
310 if (replica) this._nameReplicas.push(replica)
311 }
312
292313 // Warm read-only replicas of other publishers' release registries.
293314 // These stay live in the background via autobase update events —
294315 // CLI queries them via UDS instead of opening one-shot replicas.
……@@ -461,6 +482,12 @@ export class Sidecar {
461482 try { await r.store.close() } catch {}
462483 }
463484 this._releaseReplicas = []
485 for (const r of this._nameReplicas) {
486 try { await r.swarm.destroy() } catch {}
487 try { await r.registry.close() } catch {}
488 try { await r.store.close() } catch {}
489 }
490 this._nameReplicas = []
464491 if (this._deployServer) {
465492 try { await this._deployServer.close() } catch {}
466493 this._deployServer = null
……@@ -1088,6 +1115,55 @@ export class Sidecar {
10881115 }
10891116 }
10901117
1118 /**
1119 * Open a read-only replica of another publisher's NameRegistry, joined
1120 * on its discoveryKey so it stays warm. Lets `resolve-name` find names
1121 * claimed in registries this sidecar doesn't own. Mirrors
1122 * _openReleaseReplica. Returns { keyHex, store, registry, swarm } or null.
1123 */
1124 async _openNameReplica (keyHex) {
1125 try {
1126 const dir = path.join(homedir(), '.config', 'pgh', 'name-registries', keyHex.slice(0, 16))
1127 fs.mkdirSync(dir, { recursive: true })
1128 const store = new Corestore(dir)
1129 const registry = new NameRegistry(store, { bootstrap: keyHex })
1130 await registry.ready()
1131 const swarm = new Hyperswarm()
1132 swarm.on('error', (err) => {
1133 console.error('[pgh-sidecar] name-replica-swarm(' + keyHex.slice(0, 12) + ') error (non-fatal):', err.code || err.message)
1134 })
1135 swarm.on('connection', (conn) => {
1136 conn.on('error', () => {})
1137 store.replicate(conn)
1138 })
1139 swarm.join(registry.discoveryKey, { server: true, client: true })
1140 await swarm.flush()
1141 console.log('[pgh-sidecar] name-registry replica live: ' + keyHex.slice(0, 12) + '…')
1142 return { keyHex, store, registry, swarm }
1143 } catch (err) {
1144 console.error('[pgh-sidecar] failed to open name-registry replica ' + keyHex.slice(0, 12) + ':', err.message)
1145 return null
1146 }
1147 }
1148
1149 /**
1150 * Resolve a bare name across the own NameRegistry first, then every
1151 * subscribed read-only replica. Returns the entry or null.
1152 */
1153 async _resolveNameAcross (name) {
1154 let entry = null
1155 try { entry = await this._nameRegistry.resolve(name) } catch {}
1156 if (entry) return entry
1157 for (const r of this._nameReplicas) {
1158 try {
1159 await r.registry.update()
1160 const e = await r.registry.resolve(name)
1161 if (e) return e
1162 } catch {}
1163 }
1164 return null
1165 }
1166
10911167 /**
10921168 * Find a live registry (own writable or warm replica) for the given
10931169 * registryKeyHex. Returns the ReleaseRegistry instance or null.
……@@ -1304,10 +1380,39 @@ export class Sidecar {
13041380
13051381 svc.handle('resolve-name', async (ctx, params) => {
13061382 if (!params || !params.name) throw new Error('resolve-name requires { name }')
1307 const entry = await this._nameRegistry.resolve(params.name)
1383 // Own registry first, then subscribed read-only replicas.
1384 const entry = await this._resolveNameAcross(params.name)
13081385 return entry || null
13091386 })
13101387
1388 // Subscribe to another publisher's NameRegistry by autobase key:
1389 // persists to subscriptions.json AND opens a live read-only replica
1390 // immediately (no sidecar restart). Idempotent.
1391 svc.handle('subscribe-name-registry', async (ctx, params) => {
1392 const keyHex = params && params.registryKey
1393 if (!keyHex || typeof keyHex !== 'string' || keyHex.length !== 64) {
1394 throw new Error('subscribe-name-registry requires { registryKey } (64-char hex)')
1395 }
1396 const ownNameKey = this._nameRegistry && this._nameRegistry.keyHex
1397 if (keyHex === ownNameKey) {
1398 return { ok: true, alreadyOwn: true, subscriptions: this._nameReplicas.map(r => r.keyHex) }
1399 }
1400 const persisted = addNameSubscription(keyHex)
1401 let opened = false
1402 if (!this._nameReplicas.some(r => r.keyHex === keyHex)) {
1403 const replica = await this._openNameReplica(keyHex)
1404 if (replica) { this._nameReplicas.push(replica); opened = true }
1405 else throw new Error('failed to open name-registry replica for ' + keyHex.slice(0, 12))
1406 }
1407 return { ok: true, persisted, opened, subscriptions: this._nameReplicas.map(r => r.keyHex) }
1408 })
1409
1410 // List name-registry subscriptions (own key + replica keys).
1411 svc.handle('name-subscriptions', async () => ({
1412 own: this._nameRegistry ? this._nameRegistry.keyHex : null,
1413 replicas: this._nameReplicas.map(r => ({ keyHex: r.keyHex, writable: r.registry.writable }))
1414 }))
1415
13111416 svc.handle('claim-name', async (ctx, params) => {
13121417 if (!params || !params.name) throw new Error('claim-name requires { name }')
13131418 if (!this._nameRegistry.writable) {
……@@ -1351,7 +1456,18 @@ export class Sidecar {
13511456 })
13521457
13531458 svc.handle('list-names', async (ctx, params) => {
1354 return await this._nameRegistry.list(params || {})
1459 // Merge own registry + subscribed replicas, dedup by name (own wins).
1460 const seen = new Map()
1461 const own = await this._nameRegistry.list(params || {})
1462 for (const e of (own || [])) if (e && e.name) seen.set(e.name, e)
1463 for (const r of this._nameReplicas) {
1464 try {
1465 await r.registry.update()
1466 const list = await r.registry.list(params || {})
1467 for (const e of (list || [])) if (e && e.name && !seen.has(e.name)) seen.set(e.name, e)
1468 } catch {}
1469 }
1470 return [...seen.values()]
13551471 })
13561472
13571473 // Stubs — declared to make the UDS surface predictable. Real logic
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh-sidecar",
3 "version": "0.2.35",
3 "version": "0.2.36",
44 "description": "Small optional user-level daemon for pgh — shared identity vault, OTA polling, device linking, bootstrap seed, NameRegistry host",
55 "type": "module",
66 "main": "lib/index.js",