refactor: move document transcription, annotation, viewer sub-packages
- transcription/: TranscriptionBlock, Column, EditView, PanelHeader, ReadView, Section + transcriptionMarkers, blockConflictMerge, saveBlockWithConflictRetry + useBlockAutoSave, useBlockDragDrop hooks - annotation/: AnnotationLayer, AnnotationShape, AnnotationEditOverlay - viewer/: PdfViewer, PdfControls + useFileLoader, usePdfRenderer hooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import type { PersonMention, TranscriptionBlockData } from '$lib/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<TranscriptionBlockData> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user