OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface / lib / client.js

62 lines · javascript ref:
// The daemon client — newline-delimited JSON over the peer's unix socket.
//
// This is the ONLY thing the surface knows how to do. It holds no data, derives nothing, and caches
// nothing: every page is a question asked of favela-peer and answered by it. That is the boundary
// the README states, and it is what keeps a renderer from quietly becoming a second peer.

import net from '#node:net'

export class Client {
  constructor (sock) {
    this.sock = sock
  }

  // ⛔ Settle on every path, and never resolve without a response.
  //
  // The first version only resolved inside the `data` handler and only rejected on `error`. Against
  // a socket that does not exist it produced an empty ARRAY — so "the daemon is down" rendered as
  // "your timeline is empty", which is the exact confusion this surface exists to prevent, and it
  // was in the code under a comment saying so. A socket that closes without answering is a failure,
  // and a connect that never completes is a failure; both say so now.
  call (cmd, args = {}, { timeout = 15000 } = {}) {
    return new Promise((resolve, reject) => {
      let settled = false
      let buf = ''
      const socket = net.connect(this.sock)

      const done = (err, value) => {
        if (settled) return
        settled = true
        clearTimeout(timer)
        try { socket.destroy() } catch {}
        err ? reject(err) : resolve(value)
      }

      const timer = setTimeout(() => done(new Error('timed out talking to favela-peer at ' + this.sock)), timeout)

      socket.on('error', (err) => done(new Error(
        err.code === 'ENOENT' || err.code === 'ECONNREFUSED'
          ? 'no daemon at ' + this.sock + ' — is favela-peer running?'
          : err.message)))

      // Closed or ended with nothing back is NOT success.
      socket.on('close', () => done(new Error('favela-peer closed the connection without answering')))
      socket.on('end', () => done(new Error('favela-peer ended the connection without answering')))

      socket.on('connect', () => socket.write(JSON.stringify({ id: 1, cmd, args }) + '\n'))

      socket.on('data', (chunk) => {
        buf += chunk.toString()
        const i = buf.indexOf('\n')
        if (i === -1) return
        let res
        try { res = JSON.parse(buf.slice(0, i)) } catch (err) { return done(err) }
        if (!res || typeof res !== 'object') return done(new Error('malformed response from favela-peer'))
        done(res.ok ? null : new Error(res.error || 'favela-peer returned an error'), res.value)
      })
    })
  }
}

export default Client