fix: preserve history coverage during tail refresh
Keeps an existing complete cursor/coverage after refreshing the latest messages Prevents tail refreshes from reintroducing an incorrect "load older" state Adds a regression test for complete history coverage across refreshes
This commit is contained in:
@@ -89,6 +89,28 @@ describe("SessionMessageLoader", () => {
|
|||||||
childStores.disposeAll()
|
childStores.disposeAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preserves complete history coverage across a tail refresh", async () => {
|
||||||
|
let calls = 0
|
||||||
|
const { childStores, loader } = createLoader(async ({ sessionID }) => {
|
||||||
|
calls += 1
|
||||||
|
return calls === 1
|
||||||
|
? response([createRecord(sessionID, "msg_1")])
|
||||||
|
: response([createRecord(sessionID, "msg_2")], "stale-tail-cursor")
|
||||||
|
})
|
||||||
|
const target = { directory: "/repo", sessionID: "session-a" }
|
||||||
|
|
||||||
|
await loader.ensure(target)
|
||||||
|
expect(loader.getSnapshot(target).complete).toBe(true)
|
||||||
|
expect(loader.getSnapshot(target).cursor).toBe(undefined)
|
||||||
|
|
||||||
|
await loader.refreshTail(target, 2)
|
||||||
|
|
||||||
|
expect(loader.getSnapshot(target).complete).toBe(true)
|
||||||
|
expect(loader.getSnapshot(target).cursor).toBe(undefined)
|
||||||
|
loader.dispose()
|
||||||
|
childStores.disposeAll()
|
||||||
|
})
|
||||||
|
|
||||||
test("does not deduplicate identical session IDs across directories", async () => {
|
test("does not deduplicate identical session IDs across directories", async () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const { childStores, loader } = createLoader(async ({ directory, sessionID }) => {
|
const { childStores, loader } = createLoader(async ({ directory, sessionID }) => {
|
||||||
|
|||||||
@@ -265,18 +265,25 @@ export class SessionMessageLoader {
|
|||||||
const store = this.childStores.ensureChild(normalized.directory, { bootstrap: false })
|
const store = this.childStores.ensureChild(normalized.directory, { bootstrap: false })
|
||||||
this.bumpGeneration(entry)
|
this.bumpGeneration(entry)
|
||||||
return this.startLoad(normalized, entry, store, "refresh", async (isCurrent) => {
|
return this.startLoad(normalized, entry, store, "refresh", async (isCurrent) => {
|
||||||
|
const previousCoverage = entry.snapshot.resolved
|
||||||
|
? { cursor: entry.snapshot.cursor, complete: entry.snapshot.complete }
|
||||||
|
: null
|
||||||
const page = await this.fetchPage(normalized, Math.max(1, limit))
|
const page = await this.fetchPage(normalized, Math.max(1, limit))
|
||||||
if (!isCurrent()) return
|
if (!isCurrent()) return
|
||||||
const committed = this.commitPage(normalized, entry, store, page, "merge", isCurrent)
|
const committed = this.commitPage(normalized, entry, store, page, "merge", isCurrent)
|
||||||
if (!committed || !isCurrent()) return
|
if (!committed || !isCurrent()) return
|
||||||
|
const coverage = previousCoverage ?? page
|
||||||
this.patchEntry(entry, {
|
this.patchEntry(entry, {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
loadingKind: null,
|
loadingKind: null,
|
||||||
error: null,
|
error: null,
|
||||||
resolved: true,
|
resolved: true,
|
||||||
limit: Math.max(entry.snapshot.limit, committed.messages.length),
|
limit: Math.max(entry.snapshot.limit, committed.messages.length),
|
||||||
cursor: page.cursor,
|
// A tail refresh uses a deliberately small window. Its cursor only
|
||||||
complete: page.complete,
|
// describes that window, so it must not replace the established
|
||||||
|
// history coverage and spuriously expose "load older".
|
||||||
|
cursor: coverage.cursor,
|
||||||
|
complete: coverage.complete,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
})
|
})
|
||||||
this.persistCoverage(normalized, entry.snapshot)
|
this.persistCoverage(normalized, entry.snapshot)
|
||||||
|
|||||||
Reference in New Issue
Block a user