summaryrefslogtreecommitdiffhomepage
path: root/src/features/conversation-cache/cache.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-21 14:46:06 +0900
committerAdam Malczewski <[email protected]>2026-10-21 14:46:06 +0900
commit0bdb04b0aa0c0500e68e2e985b1a2880730ad7e8 (patch)
tree0768ddd1be8828738ee80770dd723236b91e32e8 /src/features/conversation-cache/cache.ts
parent6a2a40d64737b3cb678740c8f85027c3be0000bc (diff)
downloaddispatch-web-0bdb04b0aa0c0500e68e2e985b1a2880730ad7e8.tar.gz
dispatch-web-0bdb04b0aa0c0500e68e2e985b1a2880730ad7e8.zip
fix(review-followup-fec): reconcile epoch rollback and retain reconnect backoff
Diffstat (limited to 'src/features/conversation-cache/cache.ts')
-rw-r--r--src/features/conversation-cache/cache.ts24
1 files changed, 23 insertions, 1 deletions
diff --git a/src/features/conversation-cache/cache.ts b/src/features/conversation-cache/cache.ts
index 431a2a7..600a6e6 100644
--- a/src/features/conversation-cache/cache.ts
+++ b/src/features/conversation-cache/cache.ts
@@ -6,6 +6,19 @@ export interface ConversationCache {
historyEpoch(conversationId: string): Promise<number | undefined>;
reset(conversationId: string, historyEpoch: number): Promise<void>;
+ /**
+ * Replace from a fresh full-history response, including an epoch decrease.
+ * Only succeeds if the cache still has the epoch observed BEFORE that request.
+ * Unlike monotonic reset/commit, this is for confirmed server authority, not
+ * broadcasts or cursor/backfill responses. False means the response is stale.
+ */
+ replace(
+ conversationId: string,
+ incoming: readonly StoredChunk[],
+ historyEpoch: number,
+ expectedEpoch: number | undefined,
+ ): Promise<boolean>;
+
/** Load all cached chunks for a conversation. */
load(conversationId: string): Promise<readonly StoredChunk[]>;
@@ -73,6 +86,13 @@ export function createConversationCache(
const current = await store.historyEpoch(id);
if (current === undefined || current < epoch) await store.reset(id, epoch);
}),
+ replace: (id, incoming, epoch, expectedEpoch) =>
+ exclusive(id, async () => {
+ if ((await store.historyEpoch(id)) !== expectedEpoch) return false;
+ await store.reset(id, epoch);
+ if (incoming.length > 0) await store.append(id, incoming);
+ return true;
+ }),
async load(conversationId) {
await pending.get(conversationId);
return store.load(conversationId);
@@ -82,7 +102,9 @@ export function createConversationCache(
return exclusive(conversationId, async () => {
if (epoch !== undefined) {
const current = await store.historyEpoch(conversationId);
- if (current !== undefined && current > epoch) return store.load(conversationId);
+ // Once an epoch is known, only explicit invalidation or guarded
+ // full-history replacement may change it (in either direction).
+ if (current !== undefined && current !== epoch) return store.load(conversationId);
if (current !== epoch) await store.reset(conversationId, epoch);
}
const cached = await store.load(conversationId);