trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
996 lines (988 loc) • 27.7 kB
JavaScript
import {
init_ops,
isVcsOpKind,
verifyVcsOpHash
} from "./chunk-GRWQPKYK.js";
import {
__esm,
__export
} from "./chunk-2ESYSVXG.js";
// src/sync/reconciler.ts
var reconciler_exports = {};
__export(reconciler_exports, {
findForkPoint: () => findForkPoint,
reconcile: () => reconcile
});
function findForkPoint(opsA, opsB) {
const hashesB = new Set(opsB.map((o) => o.hash));
let forkPoint = null;
for (const op of opsA) {
if (hashesB.has(op.hash)) {
forkPoint = op.hash;
}
}
return forkPoint;
}
function reconcile(opsA, opsB) {
const forkPoint = findForkPoint(opsA, opsB);
const hashesA = new Set(opsA.map((o) => o.hash));
const hashesB = new Set(opsB.map((o) => o.hash));
const shared = [];
const uniqueToA = [];
const uniqueToB = [];
for (const op of opsA) {
if (hashesB.has(op.hash)) {
shared.push(op);
} else {
uniqueToA.push(op);
}
}
for (const op of opsB) {
if (!hashesA.has(op.hash)) {
uniqueToB.push(op);
}
}
if (uniqueToA.length === 0) {
return {
merged: [...shared, ...uniqueToB],
uniqueToA: [],
uniqueToB,
forkPoint,
clean: true,
conflicts: []
};
}
if (uniqueToB.length === 0) {
return {
merged: [...shared, ...uniqueToA],
uniqueToA,
uniqueToB: [],
forkPoint,
clean: true,
conflicts: []
};
}
const conflicts = detectConflicts(uniqueToA, uniqueToB);
const interleaved = interleaveByTimestamp(uniqueToA, uniqueToB);
return {
merged: [...shared, ...interleaved],
uniqueToA,
uniqueToB,
forkPoint,
clean: conflicts.length === 0,
conflicts
};
}
function detectConflicts(uniqueA, uniqueB) {
const conflicts = [];
const aMutations = /* @__PURE__ */ new Map();
for (const op of uniqueA) {
if (!FILE_MUTATION_KINDS.has(op.kind) || !op.vcs?.filePath) continue;
const path = op.vcs.filePath;
if (!aMutations.has(path)) aMutations.set(path, []);
aMutations.get(path).push(op);
}
for (const op of uniqueB) {
if (!FILE_MUTATION_KINDS.has(op.kind) || !op.vcs?.filePath) continue;
const path = op.vcs.filePath;
const aOps = aMutations.get(path);
if (!aOps) continue;
for (const aOp of aOps) {
if (aOp.kind === "vcs:fileModify" && op.kind === "vcs:fileModify") {
conflicts.push({
opA: aOp,
opB: op,
filePath: path,
reason: `Both sides modified ${path}`
});
} else if (aOp.kind === "vcs:fileDelete" && op.kind === "vcs:fileModify" || aOp.kind === "vcs:fileModify" && op.kind === "vcs:fileDelete") {
conflicts.push({
opA: aOp,
opB: op,
filePath: path,
reason: `Delete/modify conflict on ${path}`
});
} else if (aOp.kind === "vcs:fileAdd" && op.kind === "vcs:fileAdd") {
if (aOp.vcs?.contentHash !== op.vcs?.contentHash) {
conflicts.push({
opA: aOp,
opB: op,
filePath: path,
reason: `Both sides added ${path} with different content`
});
}
}
}
}
return conflicts;
}
function interleaveByTimestamp(a, b) {
const result = [];
let ai = 0;
let bi = 0;
while (ai < a.length && bi < b.length) {
const tA = new Date(a[ai].timestamp).getTime();
const tB = new Date(b[bi].timestamp).getTime();
if (tA <= tB) {
result.push(a[ai++]);
} else {
result.push(b[bi++]);
}
}
while (ai < a.length) result.push(a[ai++]);
while (bi < b.length) result.push(b[bi++]);
return result;
}
var FILE_MUTATION_KINDS;
var init_reconciler = __esm({
"src/sync/reconciler.ts"() {
"use strict";
FILE_MUTATION_KINDS = /* @__PURE__ */ new Set([
"vcs:fileAdd",
"vcs:fileModify",
"vcs:fileDelete",
"vcs:fileRename"
]);
}
});
// src/sync/types.ts
var PROTOCOL_VERSION = 1;
var MIN_SUPPORTED_VERSION = 1;
var MAX_SUPPORTED_VERSION = 1;
// src/sync/room-core.ts
init_ops();
// src/vcs/sync-policy.ts
import {
createHash,
randomUUID,
createCipheriv,
createDecipheriv,
createHmac,
randomBytes
} from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
var LOCAL_ONLY_OP_KINDS = ["vcs:chatMessage"];
function isLocalOnlyOpKind(kind) {
return LOCAL_ONLY_OP_KINDS.includes(kind);
}
var DEFAULT_POLICIES = {
production: {
blockRemoteDestructive: true,
bulkDeleteThreshold: 10,
requireCleanWorkingTree: true,
quarantineSuspicious: true
},
sandbox: {
blockRemoteDestructive: false,
bulkDeleteThreshold: 100,
requireCleanWorkingTree: false,
quarantineSuspicious: false
},
development: {
blockRemoteDestructive: false,
bulkDeleteThreshold: 50,
requireCleanWorkingTree: false,
quarantineSuspicious: true
}
};
function getSyncPolicy() {
const env = process.env.TRELLIS_SYNC_ENV ?? "development";
return DEFAULT_POLICIES[env] ?? DEFAULT_POLICIES.development;
}
function classifyChangeRisk(message) {
const type = message.type;
switch (type) {
case "graph-snapshot":
return { risk: "elevated", reason: "Full graph snapshot" };
case "lane-journal":
return { risk: "safe", reason: "Lane journal sync" };
case "decision-trace":
return { risk: "safe", reason: "Decision trace sync" };
case "entity-delta":
return classifyEntityDeltaRisk(message);
case "ops":
return classifyOpsRisk(message);
default:
return { risk: "safe", reason: "Unknown message type" };
}
}
function classifyEntityDeltaRisk(message) {
const { entityCount, changeTypes } = message;
if (changeTypes.includes("delete")) {
if (entityCount > 10) {
return {
risk: "destructive",
reason: `Bulk delete of ${entityCount} entities`
};
}
return { risk: "elevated", reason: "Entity deletion" };
}
if (entityCount > 100) {
return {
risk: "elevated",
reason: `Bulk modification of ${entityCount} entities`
};
}
return { risk: "safe", reason: "Entity delta sync" };
}
function classifyOpsRisk(message) {
const ops = message.ops ?? [];
const deleteOps = ops.filter(
(op) => op.kind === "delete" || op.kind === "repair"
);
const systemOps = ops.filter(
(op) => op.kind === "config" || op.kind === "agent-rule"
);
if (systemOps.length > 0) {
return {
risk: "critical",
reason: "System configuration or agent rule modification"
};
}
if (deleteOps.length > 5) {
return {
risk: "destructive",
reason: `Bulk destructive operations (${deleteOps.length})`
};
}
if (deleteOps.length > 0) {
return { risk: "elevated", reason: "Destructive operations present" };
}
return { risk: "safe", reason: "Normal operations" };
}
function shouldBlockMessage(message, policy) {
const risk = classifyChangeRisk(message);
if (policy.blockRemoteDestructive && risk.risk === "destructive") {
return { blocked: true, reason: "destructive-op", details: risk.reason };
}
if (policy.blockRemoteDestructive && risk.risk === "critical") {
return {
blocked: true,
reason: "system-modification",
details: risk.reason
};
}
if (risk.risk === "destructive" && message.entityCount > policy.bulkDeleteThreshold) {
return { blocked: true, reason: "bulk-delete", details: risk.reason };
}
if (policy.quarantineSuspicious && (risk.risk === "elevated" || risk.risk === "destructive")) {
return {
blocked: true,
reason: "quarantine-required",
details: risk.reason
};
}
return { blocked: false };
}
var QuarantineStore = class {
entries = /* @__PURE__ */ new Map();
storagePath;
encryptionKey;
hmacKey;
constructor(storagePath = ".trellis/quarantine.json") {
this.storagePath = storagePath;
const keySeed = process.env.TRELLIS_QUARANTINE_KEY || randomUUID();
this.encryptionKey = createHash("sha256").update(keySeed + "-enc").digest();
this.hmacKey = createHash("sha256").update(keySeed + "-hmac").digest();
this.load();
}
/**
* Add an entry to quarantine.
*/
add(message, sourcePeerId, reason) {
const risk = classifyChangeRisk(message);
const id = crypto.randomUUID();
const entry = {
id,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
sourcePeerId,
message,
risk,
reason,
reviewed: false
};
this.entries.set(id, entry);
this.save();
return id;
}
/**
* Get all quarantine entries.
*/
getAll() {
return Array.from(this.entries.values()).sort(
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
}
/**
* Get a specific quarantine entry.
*/
get(id) {
return this.entries.get(id);
}
/**
* Mark an entry as reviewed.
*/
markReviewed(id) {
const entry = this.entries.get(id);
if (entry) {
entry.reviewed = true;
this.save();
}
}
/**
* Remove an entry from quarantine.
*/
remove(id) {
this.entries.delete(id);
this.save();
}
/**
* Load quarantine from disk.
*/
load() {
try {
const data = readFileSync(this.storagePath, "utf-8");
const encrypted = JSON.parse(data);
const hmac = createHmac("sha256", this.hmacKey).update(encrypted.data).digest("hex");
if (hmac !== encrypted.hmac) {
console.error(
"Quarantine store HMAC verification failed - data may be corrupted"
);
return;
}
const decipher = createDecipheriv(
"aes-256-gcm",
this.encryptionKey,
Buffer.from(encrypted.iv, "hex")
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(encrypted.data, "base64")),
decipher.final()
]);
const entries = JSON.parse(
decrypted.toString("utf-8")
);
this.entries.clear();
for (const entry of entries) {
this.entries.set(entry.id, entry);
}
} catch {
}
}
/**
* Save quarantine to disk.
*/
save() {
const data = JSON.stringify(Array.from(this.entries.values()), null, 2);
const iv = randomBytes(16);
const cipher = createCipheriv("aes-256-gcm", this.encryptionKey, iv);
const encrypted = Buffer.concat([
cipher.update(data, "utf-8"),
cipher.final()
]);
const authTag = cipher.getAuthTag();
const hmac = createHmac("sha256", this.hmacKey).update(encrypted).digest("hex");
const payload = {
data: encrypted.toString("base64"),
iv: iv.toString("hex"),
authTag: authTag.toString("hex"),
hmac
};
writeFileSync(this.storagePath, JSON.stringify(payload, null, 2));
}
};
// src/sync/room-core.ts
var DEFAULT_SNAPSHOT_MAX_OPS = 500;
var SyncRoomCore = class {
roomPeer;
peers = /* @__PURE__ */ new Map();
ops = [];
constructor(roomId = "room", roomName = roomId) {
this.roomPeer = {
id: roomId,
name: roomName
};
}
connectPeer(peerId, peerName = peerId) {
this.peers.set(peerId, {
id: peerId,
name: peerName,
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
});
}
disconnectPeer(peerId) {
this.peers.delete(peerId);
}
getRoomPeer() {
return { ...this.roomPeer };
}
peersFor(peerId) {
return [
this.getRoomPeer(),
...[...this.peers.values()].filter((peer) => peer.id !== peerId).map((peer) => ({ ...peer }))
];
}
getOps() {
return [...this.ops];
}
getOpCount() {
return this.ops.length;
}
/** Build a tail snapshot for late-joiner catch-up. */
buildSnapshot(maxOps = DEFAULT_SNAPSHOT_MAX_OPS) {
const opCount = this.ops.length;
const headHash = this.ops.at(-1)?.hash;
if (opCount <= maxOps) {
return {
headHash,
opCount,
truncated: false,
ops: [...this.ops]
};
}
return {
headHash,
opCount,
truncated: true,
ops: this.ops.slice(-maxOps)
};
}
/** Deliver a snapshot message to a connected peer. */
snapshotDeliveries(peerId, maxOps = DEFAULT_SNAPSHOT_MAX_OPS) {
const snap = this.buildSnapshot(maxOps);
return [
{
peerId,
message: {
version: PROTOCOL_VERSION,
type: "snapshot",
peerId: this.roomPeer.id,
headHash: snap.headHash,
opCount: snap.opCount,
truncated: snap.truncated,
ops: snap.ops
}
}
];
}
async receive(fromPeerId, message) {
const peer = this.peers.get(fromPeerId);
if (peer) {
peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
}
if (typeof message.version !== "number" || message.version < MIN_SUPPORTED_VERSION || message.version > MAX_SUPPORTED_VERSION) {
return [
{
peerId: fromPeerId,
message: {
version: PROTOCOL_VERSION,
type: "nack",
peerId: this.roomPeer.id,
refs: [],
reason: "protocol-version",
details: `Unsupported protocol version ${message.version}; supported range is ${MIN_SUPPORTED_VERSION}-${MAX_SUPPORTED_VERSION}.`
}
}
];
}
switch (message.type) {
case "have":
return this.handleHave(fromPeerId, message);
case "want":
return this.handleWant(fromPeerId, message);
case "sync-snapshot":
return this.snapshotDeliveries(
fromPeerId,
message.maxOps ?? DEFAULT_SNAPSHOT_MAX_OPS
);
case "ops":
return this.handleOps(fromPeerId, message.ops);
case "ack":
case "nack":
case "snapshot":
return [];
case "graph-snapshot":
case "lane-journal":
case "decision-trace":
case "entity-delta":
case "device-revoked":
return [];
}
}
async handleHave(peerId, message) {
const deliveries = this.roomOpsDeliveries(peerId, message.heads.main);
const roomHashes = new Set(this.ops.map((op) => op.hash));
const peerHead = message.heads.main;
if (message.opCount > this.ops.length || peerHead && !roomHashes.has(peerHead)) {
deliveries.push({
peerId,
message: {
version: PROTOCOL_VERSION,
type: "want",
peerId: this.roomPeer.id,
wantHashes: []
}
});
} else if (!deliveries.some(
(d) => d.message.type === "want" || d.message.type === "ops"
)) {
deliveries.push({
peerId,
message: {
version: PROTOCOL_VERSION,
type: "want",
peerId: this.roomPeer.id,
wantHashes: []
}
});
}
return deliveries;
}
async handleWant(peerId, message) {
if (message.wantHashes.length > 0) {
const wanted = new Set(message.wantHashes);
return this.opsDelivery(
peerId,
this.ops.filter((op) => wanted.has(op.hash))
);
}
if (message.maxOps !== void 0 && message.maxOps > 0 && !message.afterHash) {
return this.snapshotDeliveries(peerId, message.maxOps);
}
return this.roomOpsDeliveries(peerId, message.afterHash);
}
async handleOps(peerId, ops) {
const result = await this.appendOps(ops);
const accepted = ops.filter(
(op) => this.ops.some((roomOp) => roomOp.hash === op.hash)
);
const deliveries = [];
if (accepted.length > 0) {
deliveries.push({
peerId,
message: {
version: PROTOCOL_VERSION,
type: "ack",
peerId: this.roomPeer.id,
integrated: accepted.map((op) => op.hash)
}
});
}
if (result.acceptedOps.length > 0) {
deliveries.push(...this.broadcastOps(peerId, result.acceptedOps));
}
return deliveries;
}
async appendOps(incomingOps) {
const known = new Set(this.ops.map((op) => op.hash));
const pendingByHash = /* @__PURE__ */ new Map();
const rejected = [];
const acceptedOps = [];
let skipped = 0;
let applied = 0;
for (const op of incomingOps) {
if (known.has(op.hash) || pendingByHash.has(op.hash)) {
skipped++;
continue;
}
if (!isVcsOpKind(op.kind)) {
rejected.push({
op,
reason: "invalid-kind",
message: `Rejected non-VCS op kind '${op.kind}'.`
});
continue;
}
if (!await verifyVcsOpHash(op)) {
rejected.push({
op,
reason: "hash-mismatch",
message: `Rejected op with mismatched hash '${op.hash}'.`
});
continue;
}
pendingByHash.set(op.hash, op);
}
let pending = [...pendingByHash.values()];
while (pending.length > 0) {
const nextPending = [];
let progressed = false;
for (const op of pending) {
if (op.previousHash && !known.has(op.previousHash)) {
nextPending.push(op);
continue;
}
this.ops.push(op);
known.add(op.hash);
acceptedOps.push(op);
applied++;
progressed = true;
}
if (!progressed) {
for (const op of nextPending) {
rejected.push({
op,
reason: "missing-dependency",
message: `Missing previousHash '${op.previousHash}' for op '${op.hash}'.`
});
}
break;
}
pending = nextPending;
}
return { applied, skipped, rejected, acceptedOps };
}
roomOpsDeliveries(peerId, afterHash) {
let ops = this.ops;
if (afterHash) {
const index = this.ops.findIndex((op) => op.hash === afterHash);
ops = index >= 0 ? this.ops.slice(index + 1) : this.ops;
}
return this.opsDelivery(peerId, ops);
}
opsDelivery(peerId, ops) {
if (ops.length === 0) return [];
const deliverable = ops.filter((op) => !isLocalOnlyOpKind(op.kind));
if (deliverable.length === 0) return [];
return [
{
peerId,
message: {
version: PROTOCOL_VERSION,
type: "ops",
peerId: this.roomPeer.id,
ops: deliverable
}
}
];
}
broadcastOps(fromPeerId, ops) {
const deliveries = [];
for (const peerId of this.peers.keys()) {
if (peerId === fromPeerId) continue;
deliveries.push(...this.opsDelivery(peerId, ops));
}
return deliveries;
}
};
// src/sync/sync-engine.ts
init_reconciler();
var SyncEngine = class {
localPeerId;
state;
transport;
getLocalOps;
onOpsReceived;
onNackReceived;
onDeviceRevoked;
branchPolicy;
constructor(opts) {
this.localPeerId = opts.localPeerId;
this.transport = opts.transport;
this.getLocalOps = opts.getLocalOps;
this.onOpsReceived = opts.onOpsReceived;
this.onNackReceived = opts.onNackReceived;
this.onDeviceRevoked = opts.onDeviceRevoked;
this.branchPolicy = opts.branchPolicy ?? { linear: true };
this.state = {
localPeerId: opts.localPeerId,
peerHeads: /* @__PURE__ */ new Map(),
pendingAcks: /* @__PURE__ */ new Set(),
lastSync: /* @__PURE__ */ new Map()
};
this.transport.onMessage((msg) => this.handleMessage(msg));
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
/**
* Initiate a sync with a specific peer.
* Sends a 'have' message advertising our heads.
*/
async pushTo(peerId) {
const ops = this.getLocalOps();
const heads = {};
if (ops.length > 0) {
heads["main"] = ops[ops.length - 1].hash;
}
await this.transport.send(peerId, {
version: PROTOCOL_VERSION,
type: "have",
peerId: this.localPeerId,
heads,
opCount: ops.length
});
}
/**
* Request ops from a peer.
*/
async pullFrom(peerId) {
const ops = this.getLocalOps();
const lastHash = ops.length > 0 ? ops[ops.length - 1].hash : void 0;
await this.transport.send(peerId, {
version: PROTOCOL_VERSION,
type: "want",
peerId: this.localPeerId,
wantHashes: [],
afterHash: lastHash
});
}
/**
* Request the peer's complete op set and rely on hash dedupe during ingest.
*/
async pullAllFrom(peerId) {
await this.transport.send(peerId, {
version: PROTOCOL_VERSION,
type: "want",
peerId: this.localPeerId,
wantHashes: []
});
}
/**
* Request a truncated tail snapshot from a room peer (late-joiner catch-up).
* The room replies with a `snapshot` message handled like `ops`.
*/
async requestSnapshot(peerId, maxOps = DEFAULT_SNAPSHOT_MAX_OPS) {
await this.transport.send(peerId, {
version: PROTOCOL_VERSION,
type: "sync-snapshot",
peerId: this.localPeerId,
maxOps
});
}
/**
* Send all our ops to a peer (full push).
*/
async sendOps(peerId, ops) {
const opsToSend = ops ?? this.getLocalOps();
await this.sendOpsMessage(peerId, opsToSend);
}
/**
* Internal: send an `ops` message and track outbound hashes in
* `pendingAcks` until the receiver acks or nacks them. All three sites
* that emit `ops` (sendOps, handleHave, handleWant) route through here so
* pendingAcks reflects every outbound op uniformly.
*/
async sendOpsMessage(peerId, ops) {
if (ops.length === 0) return;
for (const op of ops) {
this.state.pendingAcks.add(op.hash);
}
await this.transport.send(peerId, {
version: PROTOCOL_VERSION,
type: "ops",
peerId: this.localPeerId,
ops
});
}
/**
* Reconcile our ops with a remote peer's ops.
*/
reconcileWith(remoteOps) {
const localOps = this.getLocalOps();
return reconcile(localOps, remoteOps);
}
/**
* Get current sync state.
*/
getState() {
return this.state;
}
/**
* Get branch policy.
*/
getBranchPolicy() {
return this.branchPolicy;
}
/**
* Set branch policy.
*/
setBranchPolicy(policy) {
this.branchPolicy = policy;
}
/**
* List known peers.
*/
listPeers() {
return this.transport.peers();
}
// -------------------------------------------------------------------------
// Message handling
// -------------------------------------------------------------------------
async handleMessage(msg) {
if (typeof msg.version !== "number" || msg.version < MIN_SUPPORTED_VERSION || msg.version > MAX_SUPPORTED_VERSION) {
await this.transport.send(msg.peerId, {
version: PROTOCOL_VERSION,
type: "nack",
peerId: this.localPeerId,
refs: [],
reason: "protocol-version",
details: `Unsupported protocol version ${msg.version}; supported range is ${MIN_SUPPORTED_VERSION}-${MAX_SUPPORTED_VERSION}.`
});
return;
}
switch (msg.type) {
case "have":
await this.handleHave(msg);
break;
case "want":
await this.handleWant(msg);
break;
case "ops":
await this.handleOps(msg);
break;
case "ack":
this.handleAck(msg);
break;
case "nack":
await this.handleNack(msg);
break;
case "snapshot":
await this.handleSnapshot(msg);
break;
case "sync-snapshot":
break;
case "device-revoked":
await this.handleDeviceRevoked(msg);
break;
}
}
async handleDeviceRevoked(msg) {
await this.onDeviceRevoked?.(msg);
}
async handleSnapshot(msg) {
if (msg.ops.length === 0) return;
await this.handleOps({
version: msg.version,
type: "ops",
peerId: msg.peerId,
ops: msg.ops
});
}
async handleHave(msg) {
this.state.peerHeads.set(msg.peerId, msg.heads);
const localOps = this.getLocalOps();
const localHashes = new Set(localOps.map((o) => o.hash));
for (const [, hash] of Object.entries(msg.heads)) {
if (!localHashes.has(hash)) {
const afterHash = msg.opCount > localOps.length ? void 0 : localOps.length > 0 ? localOps[localOps.length - 1].hash : void 0;
await this.transport.send(msg.peerId, {
version: PROTOCOL_VERSION,
type: "want",
peerId: this.localPeerId,
wantHashes: [],
afterHash
});
return;
}
}
if (msg.opCount > localOps.length) {
await this.transport.send(msg.peerId, {
version: PROTOCOL_VERSION,
type: "want",
peerId: this.localPeerId,
wantHashes: []
});
return;
}
const peerOpCount = msg.opCount;
if (localOps.length > peerOpCount) {
await this.sendOpsMessage(msg.peerId, localOps.slice(peerOpCount));
}
}
async handleWant(msg) {
const localOps = this.getLocalOps();
let opsToSend;
if (msg.afterHash) {
const idx = localOps.findIndex((o) => o.hash === msg.afterHash);
opsToSend = idx >= 0 ? localOps.slice(idx + 1) : localOps;
} else if (msg.wantHashes.length > 0) {
const wanted = new Set(msg.wantHashes);
opsToSend = localOps.filter((o) => wanted.has(o.hash));
} else {
opsToSend = localOps;
}
await this.sendOpsMessage(msg.peerId, opsToSend);
}
async handleOps(msg) {
if (msg.ops.length === 0) return;
let result;
if (this.branchPolicy.linear) {
const localOps = this.getLocalOps();
const localHashes = new Set(localOps.map((o) => o.hash));
const newOps = msg.ops.filter((o) => !localHashes.has(o.hash));
if (newOps.length > 0) {
result = await this.onOpsReceived(newOps) ?? void 0;
}
} else {
const reconciled = this.reconcileWith(msg.ops);
if (reconciled.uniqueToB.length > 0) {
result = await this.onOpsReceived(reconciled.uniqueToB) ?? void 0;
}
}
const rejections = result?.rejections ?? [];
if (rejections.length > 0) {
const byReason = /* @__PURE__ */ new Map();
for (const r of rejections) {
const entry = byReason.get(r.reason) ?? {
refs: [],
details: r.details
};
entry.refs.push(r.hash);
byReason.set(r.reason, entry);
}
for (const [reason, entry] of byReason) {
await this.transport.send(msg.peerId, {
version: PROTOCOL_VERSION,
type: "nack",
peerId: this.localPeerId,
refs: entry.refs,
reason,
details: entry.details
});
}
}
const rejectedSet = new Set(rejections.map((r) => r.hash));
const ackHashes = msg.ops.map((o) => o.hash).filter((h) => !rejectedSet.has(h));
if (ackHashes.length > 0) {
await this.transport.send(msg.peerId, {
version: PROTOCOL_VERSION,
type: "ack",
peerId: this.localPeerId,
integrated: ackHashes
});
}
this.state.lastSync.set(msg.peerId, (/* @__PURE__ */ new Date()).toISOString());
}
handleAck(msg) {
for (const hash of msg.integrated) {
this.state.pendingAcks.delete(hash);
}
this.state.lastSync.set(msg.peerId, (/* @__PURE__ */ new Date()).toISOString());
}
async handleNack(msg) {
for (const ref of msg.refs) {
this.state.pendingAcks.delete(ref);
}
this.state.lastSync.set(msg.peerId, (/* @__PURE__ */ new Date()).toISOString());
if (this.onNackReceived) {
await this.onNackReceived(msg);
}
}
};
export {
PROTOCOL_VERSION,
MIN_SUPPORTED_VERSION,
MAX_SUPPORTED_VERSION,
findForkPoint,
reconcile,
reconciler_exports,
init_reconciler,
getSyncPolicy,
shouldBlockMessage,
QuarantineStore,
DEFAULT_SNAPSHOT_MAX_OPS,
SyncRoomCore,
SyncEngine
};