OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / services / org-grouped-repos.js

91 lines · javascript ref:
/**
 * 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
}