- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/ - Move person relationship components to lib/person/relationship/ - Move Stammbaum components to lib/person/genealogy/ - Move HelpPopover to lib/shared/primitives/ - Update all import paths across routes, specs, and lib files - Update vi.mock() paths in server-project test files - Remove now-empty legacy directories (components/, hooks/, server/, etc.) - Update vite.config.ts coverage include paths for new structure - Update frontend/CLAUDE.md to reflect domain-based lib/ layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
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<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;
|
|
}
|