1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
import type { StoredChunk } from "@dispatch/wire";
import type { ConversationCacheIndexEntry, ReconcileResult } from "./types";
/**
* Merge authoritative seq-keyed chunks with cached chunks.
*
* Deduplicates by `seq`, produces seq-monotonic order.
* `toAppend` = the incoming chunks whose `seq` is not already in `cached`
* (exactly what to persist). Idempotent; tolerant of out-of-order/overlapping `incoming`.
*/
export function reconcileCache(
cached: readonly StoredChunk[],
incoming: readonly StoredChunk[],
): ReconcileResult {
const seen = new Set<number>();
for (const chunk of cached) {
seen.add(chunk.seq);
}
const toAppend: StoredChunk[] = [];
for (const chunk of incoming) {
if (!seen.has(chunk.seq)) {
toAppend.push(chunk);
seen.add(chunk.seq);
}
}
const merged = [...cached, ...toAppend].sort((a, b) => a.seq - b.seq);
return { merged, toAppend };
}
/**
* Return the max committed `seq`, or `0` if empty.
* This is the `?sinceSeq=` cursor for the next incremental sync.
*/
export function nextSinceSeq(cached: readonly StoredChunk[]): number {
if (cached.length === 0) return 0;
let max = 0;
for (const chunk of cached) {
if (chunk.seq > max) max = chunk.seq;
}
return max;
}
/**
* Choose conversationIds to evict to get total cached chunks under `maxChunks`.
*
* LRU eviction: oldest `lastAccess` first, tie-break smaller `maxSeq`.
* NEVER evicts the `activeConversationId`.
* Returns [] when under budget.
*/
export function selectEvictions(
index: readonly ConversationCacheIndexEntry[],
opts: { maxChunks: number; activeConversationId: string | null },
): readonly string[] {
const totalChunks = index.reduce((sum, entry) => sum + entry.chunkCount, 0);
if (totalChunks <= opts.maxChunks) return [];
const candidates = index
.filter((entry) => entry.conversationId !== opts.activeConversationId)
.sort((a, b) => {
const aAccess = a.lastAccess ?? 0;
const bAccess = b.lastAccess ?? 0;
if (aAccess !== bAccess) return aAccess - bAccess;
return a.maxSeq - b.maxSeq;
});
let remaining = totalChunks;
const evictions: string[] = [];
for (const entry of candidates) {
if (remaining <= opts.maxChunks) break;
evictions.push(entry.conversationId);
remaining -= entry.chunkCount;
}
return evictions;
}
|