mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): restore failed sends atomically (#1326)
* fix(web): restore failed sends atomically * fix(web): wait for composer draft hydration * fix(web): count restored attachments after success * fix(web): keep scratchlist copy available * fix(web): move suppressed retry errors to target session
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { migrateSuppressedSendError } from './suppressed-send-error'
|
||||
|
||||
type ErrorRecord = { id: number; restoreSuppressed: boolean; label: string }
|
||||
|
||||
const suppressed: ErrorRecord = { id: 1, restoreSuppressed: true, label: 'retry A' }
|
||||
const ordinary: ErrorRecord = { id: 2, restoreSuppressed: false, label: 'ordinary A' }
|
||||
|
||||
describe('migrateSuppressedSendError', () => {
|
||||
it('moves a suppressed retry record from source to resolved session', () => {
|
||||
expect(migrateSuppressedSendError({ A: suppressed }, 'A', 'B')).toEqual({ B: suppressed })
|
||||
})
|
||||
|
||||
it('does not move an unsuppressed record', () => {
|
||||
const errors = { A: ordinary }
|
||||
expect(migrateSuppressedSendError(errors, 'A', 'B')).toBe(errors)
|
||||
})
|
||||
|
||||
it('is a no-op when source and resolved session are the same', () => {
|
||||
const errors = { A: suppressed }
|
||||
expect(migrateSuppressedSendError(errors, 'A', 'A')).toBe(errors)
|
||||
})
|
||||
|
||||
it('supersedes a stale target record with the in-flight suppressed retry', () => {
|
||||
const target: ErrorRecord = { id: 99, restoreSuppressed: true, label: 'stale B' }
|
||||
expect(migrateSuppressedSendError({ A: suppressed, B: target }, 'A', 'B')).toEqual({ B: suppressed })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Moves a retry-suppressed inline send error to the session selected by an
|
||||
* inactive-session resume. The record stays visible while the retry runs, but
|
||||
* must follow the eventual mutation target so success/error can resolve it.
|
||||
*/
|
||||
export function migrateSuppressedSendError<T extends { restoreSuppressed: boolean }>(
|
||||
errors: Readonly<Record<string, T>>,
|
||||
sourceSessionId: string,
|
||||
resolvedSessionId: string,
|
||||
): Record<string, T> {
|
||||
if (sourceSessionId === resolvedSessionId) return errors as Record<string, T>
|
||||
const source = errors[sourceSessionId]
|
||||
if (!source?.restoreSuppressed) return errors as Record<string, T>
|
||||
|
||||
const next = { ...errors }
|
||||
delete next[sourceSessionId]
|
||||
// The retry being resumed is the authoritative in-flight operation, so it
|
||||
// intentionally supersedes any stale target-session error record.
|
||||
next[resolvedSessionId] = source
|
||||
return next
|
||||
}
|
||||
Reference in New Issue
Block a user