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
|
import type { StoredChunk } from "@dispatch/wire";
import { nextSinceSeq, reconcileCache, selectEvictions } from "./logic";
import type { ConversationChunkStore } from "./types";
export interface ConversationCache {
/** Load all cached chunks for a conversation. */
load(conversationId: string): Promise<readonly StoredChunk[]>;
/**
* Load + reconcile + append new chunks.
* Returns the merged cache (the new authoritative cache for this conversation).
*/
commit(conversationId: string, incoming: readonly StoredChunk[]): Promise<readonly StoredChunk[]>;
/** Return the `?sinceSeq=` cursor for the next incremental sync. */
sinceSeq(conversationId: string): Promise<number>;
/**
* Evict conversations over budget.
* Returns the evicted conversationIds.
*/
evictIfOverBudget(activeConversationId: string | null): Promise<readonly string[]>;
}
export interface ConversationCacheOptions {
/** Maximum total chunks across all conversations before eviction triggers. */
readonly maxChunks?: number;
}
const DEFAULT_MAX_CHUNKS = 10_000;
/**
* Create a conversation cache backed by the injected storage port.
*
* The ONLY impurity is the injected `store`; all logic delegates to pure functions.
*/
export function createConversationCache(
store: ConversationChunkStore,
opts?: ConversationCacheOptions,
): ConversationCache {
const maxChunks = opts?.maxChunks ?? DEFAULT_MAX_CHUNKS;
return {
async load(conversationId) {
return store.load(conversationId);
},
async commit(conversationId, incoming) {
const cached = await store.load(conversationId);
const { merged, toAppend } = reconcileCache(cached, incoming);
if (toAppend.length > 0) {
await store.append(conversationId, toAppend);
}
return merged;
},
async sinceSeq(conversationId) {
const cached = await store.load(conversationId);
return nextSinceSeq(cached);
},
async evictIfOverBudget(activeConversationId) {
const idx = await store.index();
const toEvict = selectEvictions(idx, { maxChunks, activeConversationId });
for (const id of toEvict) {
await store.delete(id);
}
return toEvict;
},
};
}
|