Cancel vault rereading without persisting a false document error

This commit is contained in:
2026-09-06 10:25:17 +03:00
parent 3ad9348c20
commit 2164494c32
4 changed files with 55 additions and 11 deletions
+10
View File
@@ -545,3 +545,13 @@ Teklif Asistanı dosya okuması görünümün yaşam süresi sinyalini kullanır
Testler: bekleyen OCR sırasında iptalde tek sayfa ve temizlik; geciken sayfa ediniminde temizlik; geç sonuçta iptal callback'inin ikinci kez çağrılmaması; normal tamamlanma sonrasında dinleyici temizliği. Mevcut gerçek 45 sayfalık PDF okuma testi de geçti. Gerçek tarayıcıda Tesseract çalışanı sırasında iptal kabulü bu turda yapılmadı.
Sonuç: Tüm 122 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Planın kalan kapsamı açık; canlı yayın yapılmadı.
### D01 / D06 — Depodan yeniden okumada iptal ve özel metin koruması
Kayıtlı belge okuyucusu isteğe bağlı iptal sinyalini indirme, yanıt gövdesi ve PDF/OCR çıkarımına taşır. Başlangıç oturumu sabitlenir; belge metni döndürülmeden ve PDF işlemine geçilmeden güncel oturum kontrol edilir. DocumentVault yeniden çıkarımı ekranın yaşam süresi sinyalini kullanır; ekran kapanınca okuma durdurulur.
Yeniden çıkarımda kullanıcı iptali gerçek okuma hatası gibi kaydedilmez. İptal ve hesap değişimi sonrası `read-error` veya ortak kalem yayınına devam edilmez. Ağ/dosya hatasının mevcut sürüm kontrollü hata kaydı davranışı korunur.
Test: gerçek yardımcı fonksiyon üzerinden iptal edilen DOCX okumasında yalnız bir metin isteği, sıfır hata kaydı/kalem yayını; hesap değişimi sonrasında gelen özel Office metninin reddi. Diğer belge ekranları ve çalışan Office sunucu çıkarımının iptali bu turda tamamlanmadı; tarayıcıdan depo kapanış kabulü açık.
Sonuç: Tüm 124 test ve üretim derlemesi geçti. Büyük paket uyarıları sürüyor. Plan henüz tamamlanmadı; canlı yayın yapılmadı.
+3 -2
View File
@@ -21,9 +21,10 @@ export const DocumentVault = () => {
const DocumentVaultSession = ({signedIn}) => {
const live = useRef(true);
const readLifetime = useRef(new AbortController());
const session = useRef(sessionGuard(getToken));
const current = () => live.current && session.current();
useEffect(() => { live.current = true; return () => { live.current = false; }; }, []);
useEffect(() => { live.current = true; readLifetime.current = new AbortController(); return () => { live.current = false; readLifetime.current.abort(); }; }, []);
const useOwnedState = (initial) => {
const [value, setValue] = useState(initial);
const setOwned = useCallback((next) => { if (current()) setValue(next); }, []);
@@ -47,7 +48,7 @@ const DocumentVaultSession = ({signedIn}) => {
if (!current()) return;
setBusy(true); setHata(null); setIslemeBilgi(null);
try {
const result = await retryDocumentExtraction(document);
const result = await retryDocumentExtraction(document,{signal:readLifetime.current.signal});
setIslemeBilgi(`${document.ad}: ${result.sayi || 0} kalem çıkarıldı.`);
} catch (error) { setHata(error.message); }
finally {
+19 -9
View File
@@ -1,35 +1,45 @@
import {awaitWithAbort} from './abortable.js';
import { getToken } from './auth.js';
import { reportDocumentReadError } from './account.js';
export const readableDocument = (name) => /\.(pdf|txt|md|docx|xlsx|zip)$/i.test(String(name || ''));
export async function inspectStoredDocument(document) {
export async function inspectStoredDocument(document,{signal}={}) {
const token=getToken();
const check=()=>{signal?.throwIfAborted();if(!token || token!==getToken())throw new Error('Oturum değişti; belgeyi yeniden açın.');};
check();
if (!readableDocument(document.ad)) throw new Error('Bu dosya biçiminden metin çıkarımı desteklenmiyor.');
const office = /\.(docx|xlsx|zip)$/i.test(document.ad);
const response = await fetch(`/api/me/documents/${office ? 'text' : 'download'}?id=${encodeURIComponent(document.id)}`, {
headers: {Authorization:`Bearer ${getToken()}`},
signal, headers: {Authorization:`Bearer ${token}`},
});
if (office) {
const result = await response.json().catch(() => ({}));
const result = await awaitWithAbort(response.json(),signal);
check();
if (!response.ok || !result.success) throw new Error(result.error || 'Belge metni alınamadı.');
return {text:result.metin,format:document.ad.split('.').pop()};
}
if (!response.ok) throw new Error('Belge indirilemedi.');
const blob = await response.blob();
const blob = await awaitWithAbort(response.blob(),signal);
check();
const { extractText } = await import('./pdfText');
return extractText(new File([blob],document.ad,{type:document.mime || 'application/octet-stream'}));
check();
const result=await extractText(new File([blob],document.ad,{type:document.mime || 'application/octet-stream'}),{signal});
check();
return result;
}
export async function readStoredDocument(document) { return (await inspectStoredDocument(document)).text; }
export async function readStoredDocument(document,options) { return (await inspectStoredDocument(document,options)).text; }
export async function retryDocumentExtraction(document) {
export async function retryDocumentExtraction(document,{signal}={}) {
if (document.tur !== 'ihale' || !document.ikn) throw new Error('İhaleye bağlı ihale dokümanı gerekli.');
const token = getToken();
const checkSession = () => { if (!token || token !== getToken()) throw new Error('Oturum değişti; belgeyi yeniden açın.'); };
const checkSession = () => { signal?.throwIfAborted(); if (!token || token !== getToken()) throw new Error('Oturum değişti; belgeyi yeniden açın.'); };
checkSession();
let text;
try { text = await readStoredDocument(document); }
try { text = await readStoredDocument(document,{signal}); }
catch (error) {
if(error.name === 'AbortError') throw error;
checkSession();
try { await reportDocumentReadError(document.id,document.isleme); }
catch { throw new Error(`${error.message} (hata durumu kaydedilemedi; listeyi yenileyin)`); }
@@ -0,0 +1,23 @@
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {retryDocumentExtraction,inspectStoredDocument} from '../src/services/storedDocument.js';
test('cancelling stored document reading never writes a read error or publishes items',async()=>{
const storage=Object.getOwnPropertyDescriptor(globalThis,'localStorage'),original=globalThis.fetch;
Object.defineProperty(globalThis,'localStorage',{configurable:true,value:{getItem:()=>'qa-token'}});
const c=new AbortController();const calls=[];
try {
globalThis.fetch=async(url,{signal})=>{calls.push(url);return new Promise((resolve,reject)=>{
signal.addEventListener('abort',()=>reject(new DOMException('Aborted','AbortError')),{once:true});c.abort();
});};
await assert.rejects(retryDocumentExtraction({id:'doc',ad:'source.docx',tur:'ihale',ikn:'2026/123'},{signal:c.signal}),{name:'AbortError'});
assert.deepEqual(calls,['/api/me/documents/text?id=doc']);
} finally {globalThis.fetch=original;if(storage)Object.defineProperty(globalThis,'localStorage',storage);else delete globalThis.localStorage;}
});
test('late private office text is discarded when account changes',async()=>{
const storage=Object.getOwnPropertyDescriptor(globalThis,'localStorage'),original=globalThis.fetch;let token='first';
Object.defineProperty(globalThis,'localStorage',{configurable:true,value:{getItem:()=>token}});
try {
globalThis.fetch=async()=>{token='second';return Response.json({success:true,metin:'Private source'});};
await assert.rejects(inspectStoredDocument({id:'doc',ad:'source.docx'}),/Oturum değişti/);
} finally {globalThis.fetch=original;if(storage)Object.defineProperty(globalThis,'localStorage',storage);else delete globalThis.localStorage;}
});