From 2164494c32f48261e5b3a2e9fca2eaddcde0cb96 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:25:17 +0300 Subject: [PATCH] Cancel vault rereading without persisting a false document error --- docs/quality/uygulama-durumu.md | 10 ++++++++ src/components/tools/DocumentVault.jsx | 5 ++-- src/services/storedDocument.js | 28 ++++++++++++++------- tests/stored-document-cancellation.test.mjs | 23 +++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 tests/stored-document-cancellation.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index a4b366a..1b7aca5 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -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ı. diff --git a/src/components/tools/DocumentVault.jsx b/src/components/tools/DocumentVault.jsx index 624ba8a..b1d7207 100644 --- a/src/components/tools/DocumentVault.jsx +++ b/src/components/tools/DocumentVault.jsx @@ -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 { diff --git a/src/services/storedDocument.js b/src/services/storedDocument.js index f0c8d7a..7faba9f 100644 --- a/src/services/storedDocument.js +++ b/src/services/storedDocument.js @@ -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)`); } diff --git a/tests/stored-document-cancellation.test.mjs b/tests/stored-document-cancellation.test.mjs new file mode 100644 index 0000000..ae56e13 --- /dev/null +++ b/tests/stored-document-cancellation.test.mjs @@ -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;} +});