refactor(sync): replace message reloads with incremental tail sync

This commit is contained in:
weishu
2026-07-28 12:20:53 +08:00
parent 2235b924a7
commit faf70c64dd
28 changed files with 2927 additions and 2077 deletions
+71
View File
@@ -24,6 +24,7 @@ describe('cancelQueuedMessage', () => {
// Row should be gone from uninvoked list
const remaining = store.messages.getUninvokedLocalMessages(session.id)
expect(remaining).toHaveLength(0)
expect(store.messages.getMessageEpoch(session.id)).toBe(1)
})
it('already-invoked: returns status=invoked with full message row, row stays in DB', () => {
@@ -67,6 +68,7 @@ describe('cancelQueuedMessage', () => {
if (second.status === 'cancelled') {
expect(second.localId).toBeNull()
}
expect(store.messages.getMessageEpoch(session.id)).toBe(1)
})
it('non-existent messageId: returns status=cancelled with localId=null', () => {
@@ -173,6 +175,75 @@ describe('cancelQueuedMessage', () => {
})
})
describe('position pagination and structural epochs', () => {
it('returns rows strictly after a cursor and respects an inclusive snapshot head', () => {
const store = makeStore()
const session = makeSession(store, 'position-after')
const first = store.messages.addMessage(session.id, { text: 'first' })
const second = store.messages.addMessage(session.id, { text: 'second' })
const third = store.messages.addMessage(session.id, { text: 'third' })
store.messages.addMessage(session.id, { text: 'fourth' })
const rows = store.messages.getMessagesAfterPosition(
session.id,
10,
{ at: first.invokedAt ?? first.createdAt, seq: first.seq },
{ at: third.invokedAt ?? third.createdAt, seq: third.seq }
)
expect(rows.map((message) => message.id)).toEqual([second.id, third.id])
})
it('reports the newest composite position', () => {
const store = makeStore()
const session = makeSession(store, 'position-head')
const first = store.messages.addMessage(session.id, { text: 'first' })
const second = store.messages.addMessage(session.id, { text: 'second' })
expect(store.messages.getNewestMessagePosition(session.id)).toEqual({
at: second.invokedAt ?? second.createdAt,
seq: second.seq
})
expect(first.seq).toBeLessThan(second.seq)
})
it('bumps both epochs when session history is merged', () => {
const store = makeStore()
const source = makeSession(store, 'epoch-merge-source')
const target = makeSession(store, 'epoch-merge-target')
store.messages.addMessage(source.id, { text: 'source' })
store.messages.addMessage(target.id, { text: 'target' })
const result = store.messages.mergeSessionMessages(source.id, target.id)
expect(result.moved).toBe(1)
expect(store.messages.getMessageEpoch(source.id)).toBe(1)
expect(store.messages.getMessageEpoch(target.id)).toBe(1)
})
it('bumps the target epoch when a copied message lands behind the cached head', () => {
const store = makeStore()
const target = makeSession(store, 'epoch-copy-target')
const head = store.messages.addMessage(target.id, { text: 'head' })
const headPosition = {
at: head.invokedAt ?? head.createdAt,
seq: head.seq
}
const copied = store.messages.copyMessageToSession(target.id, {
content: { text: 'historical' },
createdAt: headPosition.at - 1_000,
localId: null,
invokedAt: headPosition.at - 1_000,
scheduledAt: null
})
expect(copied.seq).toBeGreaterThan(head.seq)
expect(store.messages.getMessagesAfterPosition(target.id, 10, headPosition)).toEqual([])
expect(store.messages.getMessageEpoch(target.id)).toBe(1)
})
})
describe('addMessage: scheduledAt invariants', () => {
it('rejects scheduledAt without a localId — would silently invoke immediately', () => {
const store = makeStore()