import type { PersonMention, TranscriptionBlockData } from '$lib/shared/types'; /** * Sentinel thrown by saveBlockWithConflictRetry after a 409 rename-mid-edit * has been merged into local state. Surfaces to the autosave hook as an * error (so the UI shows the retry indicator), but distinguishable from a * genuine network failure via the code. Carries the merged block snapshot * on its `merged` property so the caller can update local state without * a second roundtrip. */ export class BlockConflictResolvedError extends Error { readonly code = 'CONFLICT_RESOLVED' as const; merged?: TranscriptionBlockData; constructor(blockId: string) { super( `Block ${blockId} was rebased onto the latest server snapshot — retry to save the merged result` ); this.name = 'BlockConflictResolvedError'; } } type MergeArgs = { serverBlock: TranscriptionBlockData; localText: string; localMentions: PersonMention[]; }; /** * Resolves a 409-Conflict from the server by combining the latest server * snapshot with the transcriber's unsaved local edits (B12b). * * Rules: * - The transcriber's typed text always wins — never overwrite their input. * - Server is the source of truth for the displayName of any person it * knows about; renames that just landed on the server replace stale local * names by personId. * - Local-only mentions added since the last save are preserved. * - All non-mention fields (version, sortOrder, reviewed, updatedAt, ...) * come from the server snapshot so the next save sends the current * revision and matches the latest persisted state. */ export function mergeBlockOnConflict(args: MergeArgs): TranscriptionBlockData { const serverIds = new Set(args.serverBlock.mentionedPersons.map((m) => m.personId)); const localOnly = args.localMentions.filter((m) => !serverIds.has(m.personId)); return { ...args.serverBlock, text: args.localText, mentionedPersons: [...args.serverBlock.mentionedPersons, ...localOnly] }; }