OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore / lib / core / hyperswarm-content-transport.js

230 lines · javascript ref:
/**
 * Hyperswarm Content Transport
 *
 * Multiplexes content-by-hash requests over Hyperswarm connections.
 * Uses Protomux to multiplex alongside Hypercore replication.
 *
 * Protocol:
 * - Request: { type: 'content-request', hash, requestId }
 * - Response: { type: 'content-response', hash, content, requestId, found }
 */

import Protomux from 'protomux'
import c from 'compact-encoding'

// JSON encoding for messages
const jsonEncoding = {
  preencode (state, m) {
    c.string.preencode(state, JSON.stringify(m))
  },
  encode (state, m) {
    c.string.encode(state, JSON.stringify(m))
  },
  decode (state) {
    return JSON.parse(c.string.decode(state))
  }
}

export class HyperswarmContentTransport {
  constructor(swarm, contentSyncManager) {
    this.swarm = swarm
    this.contentSync = contentSyncManager
    this.channels = new Map() // conn -> channel

    // Register this transport with content sync manager
    this.contentSync.registerTransport(
      'hyperswarm',
      this.sendRequest.bind(this),
      this.hasActiveConnections.bind(this)
    )

    console.log('HyperswarmContentTransport: initialized')
  }

  /**
   * Attach to a Hyperswarm connection
   * Sets up Protomux channel for content sync protocol
   */
  attachToConnection(conn) {
    console.log('HyperswarmContentTransport: attaching to connection')

    try {
      // Get or create Protomux for this connection
      const mux = Protomux.from(conn)
      console.log('HyperswarmContentTransport: got Protomux instance')

      // Open a channel for our content sync protocol
      const channel = mux.createChannel({
        protocol: 'wirecore-content-sync',
        id: null,
        handshake: c.raw,
        onopen: (handshake) => {
          console.log('HyperswarmContentTransport: channel opened successfully!')
        },
        onclose: () => {
          console.log('HyperswarmContentTransport: channel closed')
          this.channels.delete(conn)
        },
        ondestroy: () => {
          console.log('HyperswarmContentTransport: channel destroyed')
          this.channels.delete(conn)
        }
      })

      if (!channel) {
        console.error('HyperswarmContentTransport: failed to create channel')
        return
      }

      console.log('HyperswarmContentTransport: channel created, adding message handler')

      // Add message handlers
      channel.addMessage({
        encoding: jsonEncoding,
        onmessage: (msg) => {
          console.log('HyperswarmContentTransport: received message:', msg.type)
          this.handleMessage(msg, conn)
        }
      })

      // Store channel for sending messages
      this.channels.set(conn, channel)

      // Open the channel with empty handshake
      console.log('HyperswarmContentTransport: opening channel...')
      channel.open(Buffer.alloc(0))

      // Cleanup on connection close
      conn.once('close', () => {
        console.log('HyperswarmContentTransport: connection closed')
        this.channels.delete(conn)
      })

      conn.once('error', (err) => {
        console.error('HyperswarmContentTransport: connection error:', err.message)
        this.channels.delete(conn)
      })
    } catch (err) {
      console.error('HyperswarmContentTransport: error in attachToConnection:', err)
    }
  }

  /**
   * Handle incoming message
   */
  async handleMessage(msg, conn) {
    if (!msg || !msg.type) {
      console.error('HyperswarmContentTransport: invalid message format')
      return
    }

    try {
      if (msg.type === 'content-request') {
        await this.handleContentRequest(msg, conn)
      } else if (msg.type === 'content-response') {
        await this.handleContentResponse(msg)
      } else {
        console.warn('HyperswarmContentTransport: unknown message type:', msg.type)
      }
    } catch (err) {
      console.error('HyperswarmContentTransport: message handling error:', err.message)
    }
  }

  /**
   * Handle incoming content request
   */
  async handleContentRequest(msg, conn) {
    const { hash, requestId } = msg

    if (!hash || !requestId) {
      console.error('HyperswarmContentTransport: invalid content-request')
      return
    }

    // Ask content sync manager to handle the request
    await this.contentSync.handleContentRequest(
      hash,
      requestId,
      (hash, content, requestId) => {
        this.sendResponse(conn, hash, content, requestId)
      }
    )
  }

  /**
   * Handle incoming content response
   */
  async handleContentResponse(msg) {
    const { hash, content, requestId, found } = msg

    if (!hash || !requestId) {
      console.error('HyperswarmContentTransport: invalid content-response')
      return
    }

    // Forward to content sync manager
    await this.contentSync.handleContentResponse(hash, content, requestId)
  }

  /**
   * Send content request to all active connections
   */
  async sendRequest(hash, requestId) {
    const message = {
      type: 'content-request',
      hash,
      requestId
    }

    // Send to all active channels
    for (const [conn, channel] of this.channels) {
      try {
        if (channel.opened) {
          channel.messages[0].send(message)
        }
      } catch (err) {
        console.error('HyperswarmContentTransport: failed to send request:', err.message)
      }
    }
  }

  /**
   * Send content response to a specific connection
   */
  async sendResponse(conn, hash, content, requestId) {
    const message = {
      type: 'content-response',
      hash,
      content: content || null,
      found: !!content,
      requestId
    }

    try {
      const channel = this.channels.get(conn)
      if (channel && channel.opened) {
        channel.messages[0].send(message)
      }
    } catch (err) {
      console.error('HyperswarmContentTransport: failed to send response:', err.message)
    }
  }

  /**
   * Check if we have any active connections
   */
  hasActiveConnections() {
    return this.channels.size > 0
  }

  /**
   * Get connection count
   */
  getConnectionCount() {
    return this.channels.size
  }
}

export default HyperswarmContentTransport