UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

172 lines (170 loc) 5.26 kB
// src/sync/iroh-transport.ts import { Endpoint, EndpointTicket, EndpointAddr, presetMinimal } from "@number0/iroh"; var TRELLIS_SYNC_ALPN = Array.from( Buffer.from("trellis-sync/1") ); function encodeMessage(msg) { const json = JSON.stringify(msg); return Array.from(new TextEncoder().encode(json)); } async function decodeMessage(recv, maxBytes = 16 * 1024 * 1024) { const body = await recv.readToEnd(maxBytes); if (body.length === 0) return null; const json = new TextDecoder().decode(Uint8Array.from(body)); return JSON.parse(json); } var IrohSyncTransport = class _IrohSyncTransport { endpoint; handler = null; peerAddrs = /* @__PURE__ */ new Map(); peerNames = /* @__PURE__ */ new Map(); acceptLoopRunning = false; pendingMessages = []; constructor(endpoint) { this.endpoint = endpoint; } /** * Create a new Iroh sync transport. * Binds an endpoint with the trellis-sync ALPN and starts accepting. */ static async create(opts) { let endpoint; if (opts?.endpoint) { endpoint = opts.endpoint; } else if (opts?.disableRelay) { const builder = Endpoint.builder(); presetMinimal(builder); builder.alpns([TRELLIS_SYNC_ALPN]); if (opts.secretKey) builder.secretKey(opts.secretKey); endpoint = await builder.bind(); } else { endpoint = await Endpoint.bind({ alpns: [TRELLIS_SYNC_ALPN], secretKey: opts?.secretKey }); } const transport = new _IrohSyncTransport(endpoint); transport.startAcceptLoop(); return transport; } /** * Get a ticket string to share with peers. */ ticket() { return EndpointTicket.fromAddr(this.endpoint.addr()).toString(); } /** * Connect to a remote peer by ticket string. */ async connectToPeer(ticketStr, peerId, peerName) { const addr = EndpointTicket.fromString(ticketStr).endpointAddr(); this.peerAddrs.set(peerId, addr); this.peerNames.set(peerId, peerName ?? peerId); } /** * Register a peer by explicit EndpointAddr (for programmatic use). */ addPeer(peerId, addr, name) { this.peerAddrs.set(peerId, addr); this.peerNames.set(peerId, name ?? peerId); } /** The local endpoint's ID (hex string). */ localId() { return this.endpoint.id().toString(); } // ------------------------------------------------------------------------- // SyncTransport interface // ------------------------------------------------------------------------- async send(peerId, message) { const addr = this.peerAddrs.get(peerId); if (!addr) { throw new Error(`Unknown peer: ${peerId}. Call connectToPeer() first.`); } const conn = await this.endpoint.connect(addr, TRELLIS_SYNC_ALPN); const bi = await conn.openBi(); const framed = encodeMessage(message); await bi.send.writeAll(framed); await bi.send.finish(); } onMessage(handler) { this.handler = handler; for (const msg of this.pendingMessages) { handler(msg); } this.pendingMessages = []; } peers() { const result = []; for (const [id] of this.peerAddrs) { result.push({ id, name: this.peerNames.get(id) ?? id }); } return result; } /** Connect (no-op — Iroh endpoint is always listening after create). */ async connect() { } /** Disconnect (no-op — the endpoint stays open until close()). */ async disconnect() { } /** Tear down the endpoint. */ async close() { await this.endpoint.close(); } // ------------------------------------------------------------------------- // Accept loop (background) // ------------------------------------------------------------------------- startAcceptLoop() { if (this.acceptLoopRunning) return; this.acceptLoopRunning = true; const loop = async () => { while (!this.endpoint.isClosed()) { try { const incoming = await this.endpoint.acceptNext(); if (!incoming) break; const remoteAddrInfo = await incoming.remoteAddr(); const conn = await (await incoming.accept()).connect(); this.handleIncoming(conn, remoteAddrInfo).catch(() => { }); } catch { break; } } }; loop().catch(() => { }); } async handleIncoming(conn, remoteAddrInfo) { try { const remoteId = conn.remoteId().toString(); const remoteIdStr = remoteId; if (!this.peerAddrs.has(remoteIdStr)) { this.peerNames.set(remoteIdStr, remoteIdStr); let addr = await this.endpoint.remoteAddr(conn.remoteId()); if (!addr) { const directAddrs = remoteAddrInfo.addr ? [remoteAddrInfo.addr] : []; addr = new EndpointAddr(conn.remoteId(), remoteAddrInfo.endpointId ?? null, directAddrs); } if (addr) { this.peerAddrs.set(remoteIdStr, addr); } } const bi = await conn.acceptBi(); const msg = await decodeMessage(bi.recv); if (msg) { if (this.handler) { await this.handler(msg); } else { this.pendingMessages.push(msg); } } } catch (err) { console.error("[iroh-transport] accept error:", err); } } }; export { IrohSyncTransport };