import type { PersonMention, TranscriptionBlockData } from '$lib/shared/types'; import { BlockConflictResolvedError, mergeBlockOnConflict } from '$lib/document/transcription/blockConflictMerge'; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; type Args = { fetchImpl: typeof fetch; documentId: string; blockId: string; text: string; mentionedPersons: PersonMention[]; }; /** * Persists a transcription block edit, with built-in handling for the * rename-mid-edit conflict (B12b). * * - 200/204 → resolves with the server's updated block. * - 409 → refetches the latest server block, merges it with the * transcriber's unsaved input via mergeBlockOnConflict, and * throws BlockConflictResolvedError carrying the merged * snapshot. The caller is responsible for updating local * state with `err.merged` before surfacing the error. * - other → throws Error('Save failed'). * * Validates both ids against the UUID pattern before any fetch fires * (Sina #5505 — defence-in-depth path-injection guard). */ export async function saveBlockWithConflictRetry(args: Args): Promise { const { fetchImpl, documentId, blockId, text, mentionedPersons } = args; if (!UUID_RE.test(documentId) || !UUID_RE.test(blockId)) { throw new Error(`Invalid id for save: doc=${documentId} block=${blockId}`); } const url = `/api/documents/${documentId}/transcription-blocks/${blockId}`; const res = await fetchImpl(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, mentionedPersons }) }); if (res.status === 409) { const fresh = await fetchImpl(url); if (!fresh.ok) { throw new BlockConflictResolvedError(blockId); } const serverBlock = (await fresh.json()) as TranscriptionBlockData; const merged = mergeBlockOnConflict({ serverBlock, localText: text, localMentions: mentionedPersons }); const err = new BlockConflictResolvedError(blockId); (err as BlockConflictResolvedError & { merged: TranscriptionBlockData }).merged = merged; throw err; } if (!res.ok) throw new Error('Save failed'); return (await res.json()) as TranscriptionBlockData; }