UNPKG

trellis

Version:

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

275 lines (271 loc) 7.77 kB
import { SyncEngine } from "./chunk-TIRVQJDD.js"; // src/sync/vcs-sync-peer.ts var TrellisVcsSyncPeer = class { engine; syncEngine; transport; integrationResults = []; remoteNacks = []; constructor(opts) { this.engine = opts.engine; this.transport = opts.transport; this.syncEngine = new SyncEngine({ localPeerId: opts.peerId, transport: opts.transport, getLocalOps: () => this.engine.getOps(), onOpsReceived: async (ops) => { const result = await this.engine.integrateOps(ops); this.integrationResults.push(result); await opts.onIntegrate?.(result); const rejections = result.rejected.map( (r) => ({ hash: r.op.hash, reason: r.reason, details: r.message }) ); return { rejections }; }, onNackReceived: async (nack) => { const info = { peerId: nack.peerId, reason: nack.reason, refs: nack.refs, details: nack.details }; this.remoteNacks.push(info); await opts.onRemoteNack?.(nack); }, branchPolicy: opts.branchPolicy }); } async pushTo(peerId) { return this.captureSync(peerId, () => this.syncEngine.pushTo(peerId)); } async pullFrom(peerId) { return this.captureSync(peerId, () => this.syncEngine.pullAllFrom(peerId)); } async syncWith(peerId) { return this.captureSync(peerId, async () => { await this.syncEngine.pushTo(peerId); await this.syncEngine.pullAllFrom(peerId); }); } /** Request a tail snapshot before a full sync (room peers only). */ async requestSnapshot(peerId, maxOps) { await this.syncEngine.requestSnapshot(peerId, maxOps); } listPeers() { return this.syncEngine.listPeers(); } getSyncEngine() { return this.syncEngine; } /** * Cumulative nacks received from remote peers since construction. * Useful for tests and for consumers that want to inspect the full history. * Sync results returned by `pushTo`/`pullFrom`/`syncWith` already include * the per-session slice in `remoteRejected`. */ getRemoteNacks() { return this.remoteNacks; } /** Underlying transport (for connect/close in client wrappers). */ getTransport() { return this.transport; } /** Tear down the transport connection if supported. */ close() { if ("close" in this.transport && typeof this.transport.close === "function") { this.transport.close(); } } async captureSync(peerId, run) { const beforeOpCount = this.engine.getOpCount(); const start = this.integrationResults.length; const nackStart = this.remoteNacks.length; await run(); const batches = this.integrationResults.slice(start); const remoteRejected = this.remoteNacks.slice(nackStart); const afterOpCount = this.engine.getOpCount(); return { peerId, beforeOpCount, afterOpCount, batches, applied: batches.reduce((sum, batch) => sum + batch.applied, 0), skipped: batches.reduce((sum, batch) => sum + batch.skipped, 0), rejected: batches.reduce( (sum, batch) => sum + batch.rejected.length, 0 ), remoteRejected }; } }; // src/sync/partykit-transport.ts var DEFAULT_RECONNECT = { baseDelayMs: 500, maxDelayMs: 3e4 }; var PartyKitRoomTransport = class _PartyKitRoomTransport { peerId; room; roomUrl; WebSocketImpl; ws = null; messageHandler = null; intentionalClose = false; reconnectEnabled; reconnectOpts; reconnectAttempt = 0; reconnectTimer; connectPromise = null; onReconnect; onDisconnect; constructor(opts) { this.peerId = opts.peerId; this.roomUrl = _PartyKitRoomTransport.withAuth(opts.roomUrl, opts.auth); this.room = { id: opts.roomId ?? "room", name: opts.roomName ?? opts.roomId ?? "room" }; this.onReconnect = opts.onReconnect; this.onDisconnect = opts.onDisconnect; const reconnect = opts.reconnect ?? true; this.reconnectEnabled = reconnect !== false; this.reconnectOpts = { ...DEFAULT_RECONNECT, ...reconnect === true ? {} : reconnect }; const WebSocketGlobal = globalThis.WebSocket; const WebSocketImpl = opts.WebSocketImpl ?? WebSocketGlobal; if (!WebSocketImpl) { throw new Error( "PartyKitRoomTransport requires WebSocket or opts.WebSocketImpl." ); } this.WebSocketImpl = WebSocketImpl; } async connect() { if (this.ws?.readyState === 1) return; if (this.connectPromise) return this.connectPromise; this.connectPromise = this.openSocket(); try { await this.connectPromise; } finally { this.connectPromise = null; } } async send(peerId, message) { if (peerId !== this.room.id) { throw new Error(`PartyKitRoomTransport can only send to ${this.room.id}.`); } await this.connect(); this.ws.send(JSON.stringify(message)); } onMessage(handler) { this.messageHandler = handler; } peers() { return [this.room]; } close() { this.intentionalClose = true; if (this.reconnectTimer !== void 0) { clearTimeout(this.reconnectTimer); this.reconnectTimer = void 0; } this.ws?.close(); this.ws = null; } getPeerId() { return this.peerId; } getRoomPeer() { return { ...this.room }; } /** Whether the WebSocket is open. */ isConnected() { return this.ws?.readyState === 1; } openSocket() { return new Promise((resolve, reject) => { const ws = new this.WebSocketImpl(this.roomUrl); this.ws = ws; ws.onopen = () => { this.reconnectAttempt = 0; resolve(); }; ws.onerror = (event) => { reject(new Error(`PartyKit room connection failed: ${String(event)}`)); }; ws.onmessage = async (event) => { const message = this.parseMessage(event.data); if (!message || !this.messageHandler) return; await this.messageHandler(message); }; ws.onclose = () => { if (this.ws === ws) { this.ws = null; } this.onDisconnect?.("closed"); if (!this.intentionalClose && this.reconnectEnabled) { this.scheduleReconnect(); } }; }); } scheduleReconnect() { const max = this.reconnectOpts.maxAttempts ?? 0; if (max > 0 && this.reconnectAttempt >= max) return; const base = this.reconnectOpts.baseDelayMs ?? 500; const cap = this.reconnectOpts.maxDelayMs ?? 3e4; const delay = Math.min(base * 2 ** this.reconnectAttempt, cap); this.reconnectAttempt++; if (this.reconnectTimer !== void 0) { clearTimeout(this.reconnectTimer); } this.reconnectTimer = setTimeout(() => { this.reconnectTimer = void 0; void this.tryReconnect(); }, delay); } async tryReconnect() { if (this.intentionalClose) return; try { await this.connect(); await this.onReconnect?.(); } catch { if (!this.intentionalClose && this.reconnectEnabled) { this.scheduleReconnect(); } } } static withAuth(roomUrl, auth) { if (!auth) return roomUrl; try { const url = new URL(roomUrl); url.searchParams.set("token", auth); return url.toString(); } catch { const sep = roomUrl.includes("?") ? "&" : "?"; return `${roomUrl}${sep}token=${encodeURIComponent(auth)}`; } } parseMessage(data) { try { const raw = typeof data === "string" ? data : String(data); const parsed = JSON.parse(raw); return parsed && typeof parsed.type === "string" ? parsed : null; } catch { return null; } } }; export { TrellisVcsSyncPeer, PartyKitRoomTransport };