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
JavaScript
// 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
};