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