배경
main.ts의 loadUrl/loadSelectedFile에 로드 세대(epoch) 개념이 없다. 두 로드가 겹치면 늦게 끝난 쪽이 새로 열린 문서의 상태를 그대로 덮어쓴다.
FiguresTab은 같은 문제를 #scanGeneration으로 해결했는데(#34), 문서 로드 경로에는 그에 상응하는 장치가 없다.
재현되는 증상
① 오래된 TOC가 새 문서를 덮는다. 문서 A의 outline 조회가 오래 걸리는 사이 B를 열면, A의 renderToc(...)가 B의 목차를 덮는다.
② 늦게 끝난 로드의 성공 tail이 새 문서 상태를 되돌린다. A의 initializeDoc → docData 할당, downloadName, showOnly(readRow), setPageUi(...)가 B가 이미 세팅한 값 위에 덮인다.
③ 늦게 끝난 로드의 실패가 새 문서 위에 오류 화면을 띄운다. A의 tail에서 예외가 나면 A의 catch가 figuresTab.setDocument(null) + setError(error)를 실행한다 → B는 정상 로드됐는데 오류 화면이 뜨고, B의 그림·표 탭은 #doc = null이라 영구히 스캔되지 않는다.
범위 메모
②③의 가장 날카로운 트리거였던 getOutlineItems의 미보호 getOutline() 거절은 #35에서 함수 내부 방어(try/catch + 문서 교체 감지)로 막았다. 하지만 그건 한 함수만 막은 것이고, ①②는 그대로 남아 있으며 tail에 await가 추가되면 ③도 되살아난다.
근본 해결은 #scanGeneration과 같은 모양의 로드 epoch다:
let loadEpoch = 0;
async function loadUrl(file: string) {
const epoch = ++loadEpoch;
...
const doc = await host.loadUrl(file);
if (epoch !== loadEpoch) return; // 뒤늦게 끝난 로드는 새 로드를 덮지 않는다
...
}
await 뒤 공유 상태를 건드리는 모든 지점과 catch 진입부에 같은 검사가 필요하다.
수용 기준
배경
main.ts의loadUrl/loadSelectedFile에 로드 세대(epoch) 개념이 없다. 두 로드가 겹치면 늦게 끝난 쪽이 새로 열린 문서의 상태를 그대로 덮어쓴다.FiguresTab은 같은 문제를#scanGeneration으로 해결했는데(#34), 문서 로드 경로에는 그에 상응하는 장치가 없다.재현되는 증상
① 오래된 TOC가 새 문서를 덮는다. 문서 A의 outline 조회가 오래 걸리는 사이 B를 열면, A의
renderToc(...)가 B의 목차를 덮는다.② 늦게 끝난 로드의 성공 tail이 새 문서 상태를 되돌린다. A의
initializeDoc→docData할당,downloadName,showOnly(readRow),setPageUi(...)가 B가 이미 세팅한 값 위에 덮인다.③ 늦게 끝난 로드의 실패가 새 문서 위에 오류 화면을 띄운다. A의 tail에서 예외가 나면 A의
catch가figuresTab.setDocument(null)+setError(error)를 실행한다 → B는 정상 로드됐는데 오류 화면이 뜨고, B의 그림·표 탭은#doc = null이라 영구히 스캔되지 않는다.범위 메모
②③의 가장 날카로운 트리거였던
getOutlineItems의 미보호getOutline()거절은 #35에서 함수 내부 방어(try/catch + 문서 교체 감지)로 막았다. 하지만 그건 한 함수만 막은 것이고, ①②는 그대로 남아 있으며 tail에 await가 추가되면 ③도 되살아난다.근본 해결은
#scanGeneration과 같은 모양의 로드 epoch다:await뒤 공유 상태를 건드리는 모든 지점과catch진입부에 같은 검사가 필요하다.수용 기준
docData·downloadName·TOC·표시 상태·figuresTab을 건드리지 않는다.