OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / services / index-sidecar / lib / relay-client.js

85 lines · javascript ref:
// Thin client for the relay's loopback HTTP surface. The sidecar READS the
// relay's already-public, already-redacted data (the catalogue + the signed
// capability doc) and PUBLISHES its room key back. This keeps the projection
// decoupled: identical whether the relay is local or remote, and the relay
// needs no knowledge of schema-sheets.

// Per-request timeout, optionally combined with an overall-sync deadline signal
// so one slow relay cannot wedge a whole projection pass.
async function getJSON (url, { timeoutMs = 8000, headers = {}, signal = null } = {}) {
  const signals = [AbortSignal.timeout(timeoutMs)]
  if (signal) signals.push(signal)
  try {
    const res = await fetch(url, { headers: { Accept: 'application/json', ...headers }, signal: AbortSignal.any(signals) })
    const text = await res.text()
    let body
    try { body = JSON.parse(text) } catch { body = null }
    return { ok: res.ok, status: res.status, body }
  } catch (err) {
    return { ok: false, status: 0, body: null, error: err }
  }
}

export class RelayClient {
  constructor ({ baseUrl, apiKey } = {}) {
    this.baseUrl = (baseUrl || '').replace(/\/+$/, '')
    this.apiKey = apiKey || null
  }

  _auth () {
    return this.apiKey ? { Authorization: 'Bearer ' + this.apiKey } : {}
  }

  /**
   * Fetch the full catalogue, following the /catalog.json page/pageSize
   * envelope. Aborts on the shared `signal` (overall sync deadline). Returns
   * { entries, envelope, ok } — ok:false signals an incomplete read so the
   * caller must NOT treat absent entries as deletions.
   */
  async getCatalog ({ maxPages = 25, pageSize = 200, signal = null } = {}) {
    const entries = []
    let envelope = null
    let ok = true
    for (let page = 1; page <= maxPages; page++) {
      const res = await getJSON(`${this.baseUrl}/catalog.json?page=${page}&pageSize=${pageSize}`, { signal })
      if (!res.ok || !res.body) { ok = false; break }
      const body = res.body
      if (page === 1) envelope = body
      const items = Array.isArray(body.items) ? body.items : (Array.isArray(body.entries) ? body.entries : (Array.isArray(body.catalog) ? body.catalog : []))
      entries.push(...items)
      // /catalog.json nests pagination: { page, pageSize, total }
      const pag = body.pagination || {}
      const effPageSize = typeof pag.pageSize === 'number' ? pag.pageSize : pageSize
      const total = typeof pag.total === 'number' ? pag.total : (typeof body.total === 'number' ? body.total : null)
      if (items.length < effPageSize) break
      if (total !== null && entries.length >= total) break
    }
    return { entries, envelope, ok }
  }

  /** Fetch the signed capability doc (relay self-description). */
  async getCapability ({ signal = null } = {}) {
    const res = await getJSON(`${this.baseUrl}/.well-known/hiverelay.json`, { signal })
    if (res.ok && res.body) return res.body
    const alt = await getJSON(`${this.baseUrl}/api/capabilities`, { signal })
    return alt.ok ? alt.body : null
  }

  /** Publish this relay's index-room pointer (operator-authed loopback). */
  async publishRoom (z32) {
    try {
      const r = await fetch(`${this.baseUrl}/api/manage/index-room`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...this._auth() },
        body: JSON.stringify({ room: z32 }),
        signal: AbortSignal.timeout(8000)
      })
      let body = null
      try { body = await r.json() } catch { /* ignore */ }
      return { ok: r.ok, status: r.status, body }
    } catch (err) {
      return { ok: false, status: 0, body: null, error: err }
    }
  }
}