diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 00:02:32 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 00:02:32 +0900 |
| commit | 5d9ae1849337b64af1b0d47c23b8c4950a55f792 (patch) | |
| tree | dd5fccaff7535bf1216457a986b8f95bd14fd61e /src/features/conversation-cache/cache.ts | |
| parent | fac44794432928d0341728642fd70eef87837da4 (diff) | |
| download | dispatch-web-5d9ae1849337b64af1b0d47c23b8c4950a55f792.tar.gz dispatch-web-5d9ae1849337b64af1b0d47c23b8c4950a55f792.zip | |
Slice 2 wave 1: transcript reducer, wire conformance, ws chat, cache core
- core/chunks: the one pure transcript reducer (foldEvent live deltas +
applyHistory seq-keyed reconcile + selectChunks/selectMessages); 27 tests
- core/wire: FE-side contract-conformance exhaustiveness guards + drift smoke
tests over wire/transport-contract unions (ยง2.9 drift signal); 10 tests
- adapters/ws: additively multiplex chat.send/chat.delta/chat.error on the
existing surface socket (onChat + widened send); surface API unchanged
- features/conversation-cache: pure reconcileCache/nextSinceSeq/selectEvictions
+ ConversationChunkStore port + injected createConversationCache; 26 tests
Verified green: svelte-check 0/0, vitest 169, biome clean, build ok.
Diffstat (limited to 'src/features/conversation-cache/cache.ts')
| -rw-r--r-- | src/features/conversation-cache/cache.ts | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/src/features/conversation-cache/cache.ts b/src/features/conversation-cache/cache.ts new file mode 100644 index 0000000..4aab487 --- /dev/null +++ b/src/features/conversation-cache/cache.ts @@ -0,0 +1,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; + }, + }; +} |
