/**
* Phase B: mount an org's grouped repos after a peer joins it.
*
* When a peer joins an org, the org record carries a `resourceKeys` map
* { repoName: trackerBootstrapKey } (see hyper-orgs schema). This mounts
* exactly those repos — scoped to this one org, no global repo stream, so
* the #35/#37 isolation property holds. It mirrors the join-repo path:
* bind the tracker bootstrap key, open the tracker, rendezvous on its room
* topic, and let RefReconciler materialise the bare repo from the autobase
* + overlay. Read-only for a reader joiner — it never addWriters, so push
* stays gated by the git ACL.
*
* Pure-ish + dependency-injected so the fan-out is unit-testable without a
* live swarm. `deps`:
* getOrg(name) -> org record (with resourceKeys) | null
* issueRegistry -> { bindRepoBootstrapKey, getForRepo }
* activateRoom(discKey) -> join the repo room topic (optional)
* refReconcilers -> { ensureFor(name) -> Promise } (optional)
* mounted -> Set of repo names already handled (dedup)
* onMount(name) / onError(name, err) -> observers (optional)
* announceCoords(orgName) -> publish our origin coords (writers) (optional)
* listPeerCoords(orgName) -> [{ serverKey, repoEventLog }] co-members (optional)
* bootReversePuller(serverKey,log) -> boot a scoped reverse RepoPuller (optional)
*
* The tracker mount (refs/issues) rides the autobase; the git OBJECTS ride a
* SEPARATE punch:// PACK clone (hyper-sync) that never fired for org joins.
* The announce + reverse-puller halves close that gap — the #16 bidirectional
* loop run over the ORG autobase instead of the IdentityRegistry channel.
*
* Idempotent: a repo already in `mounted`, or a name-only entry (no key),
* is skipped. Returns the `mounted` set.
*/
export async function mountOrgGroupedRepos (orgName, deps = {}) {
const {
getOrg, issueRegistry, activateRoom, refReconcilers,
mounted = new Set(), onMount, onError,
announceCoords, listPeerCoords, bootReversePuller
} = deps
if (!issueRegistry || typeof getOrg !== 'function') return mounted
let org = null
try { org = await getOrg(orgName) } catch { org = null }
if (!org || !org.resourceKeys) return mounted
for (const [repoName, bootstrapKey] of Object.entries(org.resourceKeys)) {
if (mounted.has(repoName) || !bootstrapKey) continue
try {
issueRegistry.bindRepoBootstrapKey(repoName, bootstrapKey)
const tracker = await issueRegistry.getForRepo(repoName)
if (tracker && tracker.discoveryKey && typeof activateRoom === 'function') {
activateRoom(tracker.discoveryKey)
}
if (refReconcilers && typeof refReconcilers.ensureFor === 'function') {
const p = refReconcilers.ensureFor(repoName)
if (p && typeof p.catch === 'function') p.catch(() => {})
}
mounted.add(repoName)
if (onMount) onMount(repoName)
} catch (err) {
if (onError) onError(repoName, err)
}
}
// ── Org-scoped bidirectional git sync (#16 loop over the ORG autobase) ──
// The loop above mounts trackers (refs/issues, autobase). The git OBJECTS
// still need the punch:// PACK clone, which never fired for org joins. Two
// halves, mirroring the IdentityRegistry mirror-writer flow:
// 1. ANNOUNCE our origin coords so co-members can pull FROM us — writers
// only (announceCoords self-gates on org writability; a reader can't
// append, and has nothing to serve anyway).
// 2. WATCH co-members' announced coords + boot a scoped reverse RepoPuller
// toward each, so we clone the org's repos FROM them. bootReversePuller
// is wired daemon-side with an org-scope repoFilter (#35 isolation).
if (typeof announceCoords === 'function') {
try { await announceCoords(orgName) } catch (err) { if (onError) onError('__announce__', err) }
}
if (typeof listPeerCoords === 'function' && typeof bootReversePuller === 'function') {
let peers = []
try { peers = await listPeerCoords(orgName) } catch { peers = [] }
for (const peer of (peers || [])) {
if (!peer || !peer.serverKey || !peer.repoEventLog) continue
try {
bootReversePuller(peer.serverKey, peer.repoEventLog)
} catch (err) {
if (onError) onError('__puller__:' + String(peer.serverKey).slice(0, 12), err)
}
}
}
return mounted
}