Asynchronous Compaction & Drift Prevention
Background workers that reconcile the graph against the source of truth, merge duplicate nodes, compact version histories, and detect state drift. Every operation is idempotent, logged, and checksum-verified.
How compaction works
The compaction worker runs as a background cron job. Each cycle scans for compaction candidates, detects duplicate nodes, resolves conflicts, atomically replaces duplicates, and verifies checksums.
Compaction Worker Pipeline
Runs as a background cron with configurable interval and batch size.
Scan for Candidates
Query the entity store for nodes exceeding a configurable staleness threshold or with missing checksums. Candidates are batched (default batch size: 1000) to limit memory pressure.
async function findCompactionCandidates(
store: EntityStore,
opts: { batchSize: number; stalenessDays: number },
): Promise<EntityRef[]> {
const cutoff = Date.now() - opts.stalenessDays * 86400_000;
return store.query({
filter: {
anyOf: [
{ field: "updated_at", lt: cutoff },
{ field: "checksum", eq: null },
],
},
limit: opts.batchSize,
});
}Detect Duplicates
For each candidate, compute a fingerprint using canonical keys (email domain, name similarity, reply-chain analysis). Group candidates by fingerprint and flag groups with more than one node as duplicates.
interface Fingerprint {
email: string;
nameNormalized: string;
domain: string;
}
function fingerprint(entity: Person): Fingerprint {
return {
email: entity.email.toLowerCase(),
nameNormalized: entity.name
.toLowerCase()
.replace(/[^a-z0-9]/g, ""),
domain: entity.domain.toLowerCase(),
};
}
function findDuplicates(
candidates: Person[],
): Map<string, Person[]> {
const groups = new Map<string, Person[]>();
for (const c of candidates) {
const fp = fingerprint(c);
const key = `${fp.domain}:${fp.nameNormalized}`;
const group = groups.get(key) ?? [];
group.push(c);
groups.set(key, group);
}
// Return only groups with >1 member
for (const [key, group] of groups) {
if (group.length < 2) groups.delete(key);
}
return groups;
}Resolve Conflicts
For each duplicate group, apply the configured merge strategy to produce a single canonical node. The default strategy is last-writer-wins with field-level reconciliation for arrays.
Atomic Replace
Replace all duplicate nodes with the canonical node in a single transaction. Update all incoming edges to point to the canonical node. Record the merge in the audit log with before/after checksums.
Verify Checksums
Recompute the checksum of the canonical node and persist it. If the recomputed checksum does not match the stored checksum, flag the entity for re-reconciliation with the source metadata.
Graph State Before and After
Three person nodes (j.doe, john.d, jdoe) are detected as duplicates and merged into a single canonical node. All outgoing edges are re-pointed to the canonical identity.
Graph State Before and After Compaction
Conflict Resolution
When duplicate nodes are detected, the compaction worker applies a configurable merge strategy to produce a single canonical node.
Last-Writer-Wins (Default)
For each conflicting field, the value from the node with the most recentupdated_at timestamp is selected. Array fields use set union across all duplicates. This is the safest default since it never loses data.
Field-Level Reconciliation
Each field type uses its own reconciliation rule: scalar fields use most-recent, counter fields are summed, array fields are unioned, and reference fields (edge pointers) are merged if they point to distinct targets.
Time-Ranked Composite
Nodes are ranked by data richness (number of populated fields, edge count, recency). The richest node becomes the base; fields from lower-ranked nodes fill in gaps. This produces the most complete result but is more expensive.
Manual Review Queue
When confidence in the merge is low (e.g., conflicting names from different domains), the duplicate group is marked for manual review. The compaction worker skips these groups and logs them to the review dashboard.
Merge Engine
The merge engine takes a group of duplicate nodes and a strategy, then produces a canonical node with a merge log explaining every field-level decision.
// Generic merge engine. Works with any entity type that has a
// checksum, version vector, and typed fields.
export type MergeStrategy = "last_writer_wins" | "field_level" | "time_ranked" | "manual_review";
export interface MergePlan {
canonical: object;
mergeLog: MergeEntry[];
strategy: MergeStrategy;
}
export interface MergeEntry {
field: string;
resolution: "kept" | "replaced" | "merged" | "filled";
fromValues: unknown[];
toValue: unknown;
reason: string;
}
export class MergeEngine {
merge<T extends { updated_at: number; checksum?: string }>(
duplicates: T[],
strategy: MergeStrategy,
): MergePlan {
switch (strategy) {
case "last_writer_wins":
return this.lastWriterWins(duplicates);
case "field_level":
return this.fieldLevelReconciliation(duplicates);
case "time_ranked":
return this.timeRankedComposite(duplicates);
case "manual_review":
return { canonical: duplicates[0], mergeLog: [], strategy };
default:
return this.lastWriterWins(duplicates);
}
}
private lastWriterWins<T extends { updated_at: number }>(
duplicates: T[],
): MergePlan {
const sorted = [...duplicates].sort(
(a, b) => b.updated_at - a.updated_at,
);
const canonical = { ...sorted[0] };
const mergeLog: MergeEntry[] = [];
// For each field, take the most recently written value;
// for arrays, union all values.
for (const key of Object.keys(canonical)) {
if (key === "updated_at" || key === "checksum") continue;
const values = sorted.map((d) => (d as Record<string, unknown>)[key]);
const isArray = Array.isArray(values[0]);
if (isArray) {
const merged = [...new Set(values.flat() as unknown[])];
(canonical as Record<string, unknown>)[key] = merged;
mergeLog.push({
field: key,
resolution: "merged",
fromValues: values,
toValue: merged,
reason: `Set union of ${values.length} duplicates`,
});
} else {
const winner = values[0];
mergeLog.push({
field: key,
resolution: values.slice(1).every((v) => v === winner) ? "kept" : "replaced",
fromValues: values,
toValue: winner,
reason: `Last writer wins: selected from most recent node`,
});
}
}
return { canonical, mergeLog, strategy: "last_writer_wins" };
}
private fieldLevelReconciliation<T extends { updated_at: number }>(
duplicates: T[],
): MergePlan {
const base = {} as Record<string, unknown>;
const mergeLog: MergeEntry[] = [];
const entries = Object.keys(duplicates[0] as Record<string, unknown>)
.filter((k) => k !== "checksum");
for (const key of entries) {
const values = duplicates.map((d) =>
(d as Record<string, unknown>)[key],
);
const unique = [...new Set(values.map((v) => JSON.stringify(v)))].map(
(s) => JSON.parse(s),
);
if (unique.length === 1) {
base[key] = unique[0];
continue;
}
// Resolve by field type
const sample = unique[0];
if (typeof sample === "number" && key.endsWith("_count")) {
// Counter field: sum
base[key] = (unique as number[]).reduce((a, b) => a + b, 0);
mergeLog.push({
field: key,
resolution: "merged",
fromValues: values,
toValue: base[key],
reason: `Counter: summed ${unique.length} values`,
});
} else if (Array.isArray(sample)) {
// Array field: union
base[key] = [...new Set(unique.flat() as unknown[])];
mergeLog.push({
field: key,
resolution: "merged",
fromValues: values,
toValue: base[key],
reason: `Array: set union of ${unique.length} sources`,
});
} else if (key === "updated_at") {
// Timestamp: pick max
base[key] = Math.max(...(unique as number[]));
} else {
// Scalar: pick most recent node's value
const mostRecent = [...duplicates].sort(
(a, b) => b.updated_at - a.updated_at,
)[0];
base[key] = (mostRecent as Record<string, unknown>)[key];
mergeLog.push({
field: key,
resolution: "replaced",
fromValues: values,
toValue: base[key],
reason: `Scalar: last writer wins`,
});
}
}
return {
canonical: base as T,
mergeLog,
strategy: "field_level",
};
}
private timeRankedComposite<T extends { updated_at: number }>(
duplicates: T[],
): MergePlan {
// Rank by: populated fields (desc), edge count (desc), recency (desc)
const ranked = [...duplicates].sort((a, b) => {
const aPopulated = Object.values(a as Record<string, unknown>)
.filter((v) => v !== null && v !== undefined && v !== "").length;
const bPopulated = Object.values(b as Record<string, unknown>)
.filter((v) => v !== null && v !== undefined && v !== "").length;
if (aPopulated !== bPopulated) return bPopulated - aPopulated;
return b.updated_at - a.updated_at;
});
const base = { ...ranked[0] } as Record<string, unknown>;
const mergeLog: MergeEntry[] = [];
// Fill gaps from lower-ranked nodes
for (let i = 1; i < ranked.length; i++) {
for (const [key, value] of Object.entries(ranked[i] as Record<string, unknown>)) {
if (key === "checksum" || key === "updated_at") continue;
const current = base[key];
if (current === null || current === undefined || current === "" ||
(Array.isArray(current) && current.length === 0)) {
base[key] = value;
mergeLog.push({
field: key,
resolution: "filled",
fromValues: [current, value],
toValue: value,
reason: `Gap-filled from rank-${i} node`,
});
}
}
}
return { canonical: base as T, mergeLog, strategy: "time_ranked" };
}
}Versioned Snapshots
Every compaction cycle creates a versioned snapshot of the affected entities. Snapshots enable point-in-time recovery, audit trails, and incremental drift detection.
// Manages versioned snapshots of entity state. Each snapshot
// captures the full serialized state at a point in time,
// keyed by (entity_type, entity_id, version).
export interface Snapshot {
entityType: string;
entityId: string;
version: number;
state: object;
checksum: string;
parentVersion: number | null;
createdAt: number;
compactionId: string;
}
export class SnapshotManager {
private store: SnapshotStore;
private currentVersion = new Map<string, number>();
constructor(store: SnapshotStore) {
this.store = store;
}
async takeSnapshot(
entityType: string,
entity: { id: string } & object,
compactionId: string,
): Promise<Snapshot> {
const key = `${entityType}:${(entity as Record<string, string>).id}`;
const currentVer = this.currentVersion.get(key) ?? 0;
const version = currentVer + 1;
const snapshot: Snapshot = {
entityType,
entityId: (entity as Record<string, string>).id,
version,
state: { ...entity },
checksum: computeEntityChecksum(entity),
parentVersion: currentVer > 0 ? currentVer : null,
createdAt: Date.now(),
compactionId,
};
await this.store.put(snapshot);
this.currentVersion.set(key, version);
return snapshot;
}
async restoreSnapshot(
entityType: string,
entityId: string,
version: number,
): Promise<object | null> {
return this.store.get(entityType, entityId, version);
}
async getLatestSnapshot(
entityType: string,
entityId: string,
): Promise<Snapshot | null> {
return this.store.getLatest(entityType, entityId);
}
async listSnapshots(
entityType: string,
entityId: string,
): Promise<Snapshot[]> {
return this.store.list(entityType, entityId);
}
}
// Snapshot retention: keep all snapshots for 30 days,
// then compact to daily snapshots for 90 days, then
// weekly for 1 year.
export const SNAPSHOT_RETENTION: RetentionRule[] = [
{ maxAge: 30 * 86400_000, keepAll: true },
{ maxAge: 90 * 86400_000, keepInterval: 86400_000 }, // daily
{ maxAge: 365 * 86400_000, keepInterval: 7 * 86400_000 }, // weekly
];
export async function applyRetentionPolicy(
manager: SnapshotManager,
entityType: string,
entityId: string,
): Promise<number> {
const snapshots = await manager.listSnapshots(entityType, entityId);
const now = Date.now();
let deleted = 0;
for (const snap of snapshots) {
const age = now - snap.createdAt;
const rule = SNAPSHOT_RETENTION.find((r) => age <= r.maxAge);
if (!rule) {
// Beyond all retention windows - delete
await manager.store.delete(entityType, entityId, snap.version);
deleted++;
}
// Rule-based retention is applied elsewhere
}
return deleted;
}
interface RetentionRule {
maxAge: number;
keepAll?: boolean;
keepInterval?: number;
}
interface SnapshotStore {
put(snapshot: Snapshot): Promise<void>;
get(entityType: string, entityId: string, version: number): Promise<object | null>;
getLatest(entityType: string, entityId: string): Promise<Snapshot | null>;
list(entityType: string, entityId: string): Promise<Snapshot[]>;
delete(entityType: string, entityId: string, version: number): Promise<void>;
}Snapshot Lifecycle
Recovery workflow
When drift is detected, the operator can restore an entity to any previous snapshot version. The restore replays the entity's state from the snapshot and re-runs the pipeline for incoming messages that arrived after the snapshot timestamp, re-applying only the relevant mutations.
Reconciliation Worker
The reconciliation worker runs as a background process that re-fetches source metadata for each entity, replays the pipeline, and compares checksums. When drift is found, it automatically re-merges and logs the event.
// Full reconciliation worker. Runs as a background cron that
// iterates over entities, fetches source metadata, re-plays the
// pipeline, and verifies consistency.
export class ReconciliationWorker {
private stores: EntityStores;
private graphUpdater: GraphUpdater;
private snapshotManager: SnapshotManager;
constructor(
stores: EntityStores,
graphUpdater: GraphUpdater,
snapshotManager: SnapshotManager,
) {
this.stores = stores;
this.graphUpdater = graphUpdater;
this.snapshotManager = snapshotManager;
}
async reconcileEntity(
entityType: string,
entityId: string,
): Promise<ReconciliationResult> {
// 1. Fetch the current entity state
const current = await this.stores.getEntity(entityType, entityId);
if (!current) {
return { status: "not_found", entityType, entityId };
}
// 2. Compute current checksum
const currentChecksum = computeEntityChecksum(current);
const storedChecksum = (current as Record<string, unknown>).checksum as string;
// 3. Compare checksums
if (currentChecksum === storedChecksum) {
return { status: "consistent", entityType, entityId };
}
// 4. Checksum mismatch - fetch source metadata and replay
const sourceEvents = await this.fetchSourceEvents(entityType, entityId);
if (sourceEvents.length === 0) {
// No source data: update the stored checksum to match reality
await this.stores.updateChecksum(entityType, entityId, currentChecksum);
return {
status: "checksum_corrected",
entityType,
entityId,
previousChecksum: storedChecksum,
newChecksum: currentChecksum,
};
}
// 5. Take a snapshot before re-merging
const compactionId = `reconcile:${entityType}:${entityId}:${Date.now()}`;
await this.snapshotManager.takeSnapshot(entityType, current, compactionId);
// 6. Re-merge from source events
let merged = current;
let changed = false;
for (const event of sourceEvents) {
const result = await this.graphUpdater.processEmail(event);
if (result.status === "applied") {
changed = true;
}
// Re-fetch the entity after each merge
merged = await this.stores.getEntity(entityType, entityId);
}
// 7. Recompute checksum after merge
const finalChecksum = merged
? computeEntityChecksum(merged)
: currentChecksum;
if (changed) {
await this.stores.updateChecksum(entityType, entityId, finalChecksum);
return {
status: "reconciled",
entityType,
entityId,
previousChecksum: storedChecksum,
newChecksum: finalChecksum,
eventsReplayed: sourceEvents.length,
};
}
return {
status: "checksum_updated",
entityType,
entityId,
previousChecksum: storedChecksum,
newChecksum: finalChecksum,
};
}
async reconcileAll(
entityTypes: string[],
concurrency = 4,
): Promise<ReconciliationSummary> {
const summary: ReconciliationSummary = {
total: 0,
consistent: 0,
reconciled: 0,
errors: [],
};
for (const entityType of entityTypes) {
const ids = await this.stores.listEntityIds(entityType);
summary.total += ids.length;
// Process with limited concurrency
const batches = this.chunk(ids, concurrency);
for (const batch of batches) {
const results = await Promise.allSettled(
batch.map((id) => this.reconcileEntity(entityType, id)),
);
for (const result of results) {
if (result.status === "fulfilled") {
if (result.value.status === "consistent") {
summary.consistent++;
} else if (
result.value.status === "reconciled" ||
result.value.status === "checksum_corrected"
) {
summary.reconciled++;
}
} else {
summary.errors.push(result.reason?.message ?? "unknown");
}
}
}
}
return summary;
}
private async fetchSourceEvents(
entityType: string,
entityId: string,
): Promise<EmailMetadataEvent[]> {
// Query the source mail store for all events involving this entity
const source = this.stores.getSourceStore(entityType);
if (!source) return [];
return source.queryByEntity(entityType, entityId);
}
private chunk<T>(arr: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
}
export type ReconciliationResult =
| { status: "consistent"; entityType: string; entityId: string }
| { status: "not_found"; entityType: string; entityId: string }
| { status: "checksum_corrected"; entityType: string; entityId: string; previousChecksum: string; newChecksum: string }
| { status: "reconciled"; entityType: string; entityId: string; previousChecksum: string; newChecksum: string; eventsReplayed: number }
| { status: "checksum_updated"; entityType: string; entityId: string; previousChecksum: string; newChecksum: string }
| { status: "error"; entityType: string; entityId: string; error: string };
interface ReconciliationSummary {
total: number;
consistent: number;
reconciled: number;
errors: string[];
}