mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(sync): preserve pull cursor on lock conflict
Propagate retryable import lock conflicts through CLI and Desktop pull adapters so scheduled pulls defer without advancing past unimported messages.
This commit is contained in:
1 parent
8bee0fab4a
commit
5be001fea5
6 files changed
+147
-13
No files matched your search
@@ -215,6 +215,75 @@ describe('PullEngine', () => {
|
||||
assert.deepEqual(checkedSessionIds, ['local_session_b'])
|
||||
})
|
||||
|
||||
it('keeps the pull cursor stable when an import is deferred by another writer', async () => {
|
||||
const session = createSession()
|
||||
const dataSource = createDataSource()
|
||||
dataSource.sessions = [session]
|
||||
const page = writeTempJson({
|
||||
chatlab: { version: '0.0.2', exportedAt: 100 },
|
||||
meta: { name: 'Test Session', platform: 'test', type: 'group' },
|
||||
members: [{ platformId: 'u1', accountName: 'Alice' }],
|
||||
messages: [{ sender: 'u1', timestamp: 101, type: 0, content: 'hi' }],
|
||||
sync: { hasMore: false, nextSince: 101 },
|
||||
})
|
||||
const sessionUpdates: Array<{ sessionId: string; updates: Partial<ImportSession> }> = []
|
||||
const engine = createEngine({
|
||||
files: [page],
|
||||
dataSource,
|
||||
sessionUpdates,
|
||||
importResult: {
|
||||
success: false,
|
||||
newMessageCount: 0,
|
||||
error: 'error.import_in_progress',
|
||||
retryable: true,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await withImmediateTimers(() => engine.executePullSession(dataSource.id, dataSource, session))
|
||||
|
||||
assert.equal(result.success, false)
|
||||
assert.equal(result.error, 'error.import_in_progress')
|
||||
assert.deepEqual(sessionUpdates, [])
|
||||
})
|
||||
|
||||
it('keeps the pull cursor stable when a retry-page import is deferred', async () => {
|
||||
const session = createSession()
|
||||
const dataSource = createDataSource()
|
||||
dataSource.sessions = [session]
|
||||
const emptyInitialPage = writeTempJson({
|
||||
chatlab: { version: '0.0.2', exportedAt: 100 },
|
||||
meta: { name: 'Test Session', platform: 'test', type: 'group' },
|
||||
members: [],
|
||||
messages: [],
|
||||
sync: { hasMore: false, nextSince: 100 },
|
||||
})
|
||||
const retryPage = writeTempJson({
|
||||
chatlab: { version: '0.0.2', exportedAt: 100 },
|
||||
meta: { name: 'Test Session', platform: 'test', type: 'group' },
|
||||
members: [{ platformId: 'u1', accountName: 'Alice' }],
|
||||
messages: [{ sender: 'u1', timestamp: 101, type: 0, content: 'x'.repeat(1200) }],
|
||||
sync: { hasMore: false, nextSince: 101 },
|
||||
})
|
||||
const sessionUpdates: Array<{ sessionId: string; updates: Partial<ImportSession> }> = []
|
||||
const engine = createEngine({
|
||||
files: [emptyInitialPage, retryPage],
|
||||
dataSource,
|
||||
sessionUpdates,
|
||||
importResult: {
|
||||
success: false,
|
||||
newMessageCount: 0,
|
||||
error: 'error.import_in_progress',
|
||||
retryable: true,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await withImmediateTimers(() => engine.executePullSession(dataSource.id, dataSource, session))
|
||||
|
||||
assert.equal(result.success, false)
|
||||
assert.equal(result.error, 'error.import_in_progress')
|
||||
assert.deepEqual(sessionUpdates, [])
|
||||
})
|
||||
|
||||
it('reports retry import failure instead of marking the pull successful', async () => {
|
||||
const session = createSession()
|
||||
const dataSource = createDataSource()
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
SyncLogger,
|
||||
FetchParams,
|
||||
SyncMeta,
|
||||
ImportResult,
|
||||
PullSessionResult,
|
||||
PullProgress,
|
||||
} from './types'
|
||||
@@ -223,17 +224,7 @@ export class PullEngine {
|
||||
return Array.from(this.progressMap.values())
|
||||
}
|
||||
|
||||
private async importTempFile(
|
||||
baseUrl: string,
|
||||
sess: ImportSession,
|
||||
tempFile: string
|
||||
): Promise<{
|
||||
success: boolean
|
||||
newMessageCount: number
|
||||
sessionId?: string
|
||||
error?: string
|
||||
needFullResync?: boolean
|
||||
}> {
|
||||
private async importTempFile(baseUrl: string, sess: ImportSession, tempFile: string): Promise<ImportResult> {
|
||||
let targetId = sess.targetSessionId
|
||||
if (!targetId) {
|
||||
const derived = deriveLocalSessionId(baseUrl, sess.remoteSessionId)
|
||||
@@ -247,6 +238,13 @@ export class PullEngine {
|
||||
return this.importer.importFile(tempFile, targetId || undefined, externalId)
|
||||
}
|
||||
|
||||
private deferRetryableImport(sess: ImportSession, error?: string): PullSessionResult {
|
||||
const errMsg = error || 'Import deferred'
|
||||
this.logger.info(`[Pull] Deferring "${sess.name}": ${errMsg}`)
|
||||
this.markProgressDone(sess.id)
|
||||
return { success: false, newMessageCount: 0, error: errMsg }
|
||||
}
|
||||
|
||||
async executePullSession(sourceId: string, ds: DataSource, sess: ImportSession): Promise<PullSessionResult> {
|
||||
const currentDs = this.dsManager.get(sourceId)
|
||||
if (!currentDs || !currentDs.sessions.some((s) => s.id === sess.id)) {
|
||||
@@ -342,6 +340,8 @@ export class PullEngine {
|
||||
}
|
||||
|
||||
if (!retryResult.success) {
|
||||
if (retryResult.retryable) return this.deferRetryableImport(sess, retryResult.error)
|
||||
|
||||
const errMsg = retryResult.error || 'Import failed'
|
||||
this.dsManager.updateSession(sourceId, sess.id, {
|
||||
lastPullAt: Math.floor(Date.now() / 1000),
|
||||
@@ -421,6 +421,8 @@ export class PullEngine {
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (result.retryable) return this.deferRetryableImport(sess, result.error)
|
||||
|
||||
const errMsg = result.error || 'Import failed'
|
||||
this.dsManager.updateSession(sourceId, sess.id, {
|
||||
lastPullAt: Math.floor(Date.now() / 1000),
|
||||
|
||||
@@ -93,6 +93,8 @@ export interface ImportResult {
|
||||
sessionId?: string
|
||||
error?: string
|
||||
needFullResync?: boolean
|
||||
/** The source page was not imported and should be retried without advancing the pull cursor. */
|
||||
retryable?: boolean
|
||||
}
|
||||
|
||||
export interface PullSessionResult {
|
||||
|
||||
Reference in new issue
Block a user