[PonsWarp 개념 교실 20] Resume 커서 — 끊긴 전송을 중간부터 이어가는 법
쉽게 말하면, **Resume 커서는 "이미 저장된 마지막 위치를 파일·조각·파티션 위치로 번역해 그 지점부터 다시 시작하는 좌표"**다.

전송이 끊겼을 때 처음부터 다시 보내면 시간과 데이터가 모두 낭비된다. PonsWarp는 수신측의 contiguous frontier를 global offset으로 기억하고, sender가 그 위치부터 다시 읽게 한다.
하나의 offset을 네 좌표로 바꾸기
재개에 필요한 정보는 하나의 global offset에서 시작한다.
globalOffset
├─ fileIndex 어느 파일인가
├─ fileOffset 그 파일 안의 위치
├─ sequence 몇 번째 청크인가
└─ nextPartitionEnd 다음 체크포인트
typescriptconst safeStartOffset = Math.min( Math.max(0, Math.floor(input.startOffset)), Math.max(0, input.totalSize) );
먼저 offset을 0과 전체 크기 사이로 clamp한다. 잘못된 resume 요청이 파일 범위를 벗어나지 않게 하는 기본 방어다.
파일 경계 찾기
여러 파일을 하나의 global stream으로 보내면 offset은 파일 경계를 모른다. 정책 함수는 파일 크기를 앞에서부터 더해 어느 파일에 offset이 들어가는지 찾는다.
typescriptwhile ( fileIndex < input.fileSizes.length && bytesBeforeFile + input.fileSizes[fileIndex] <= safeStartOffset ) { bytesBeforeFile += input.fileSizes[fileIndex]; fileIndex++; }
그다음 fileOffset = startOffset - bytesBeforeFile로 파일 안의 위치를 계산한다.
sequence와 partition 계산
청크 크기가 64KiB이고 offset이 1MiB라면 대략 16번째 청크부터 시작한다.
typescriptsequence = Math.floor(safeStartOffset / input.chunkSize); partitionNumber = Math.floor( safeStartOffset / input.partitionSize ) + 1;
이렇게 계산한 sequence는 nonce 전진과 패킷 헤더, sender 재시작 위치에 함께 사용된다. nextPartitionEnd는 재개 후 다음 PARTITION_ACK 경계다.
모바일 화면 꺼짐
모바일 브라우저는 페이지가 숨겨져도 곧바로 복구를 포기하지 않는다. transfer active, room, writer, fileCount가 모두 있고 재연결 예산이 남아 있으면 계속 시도한다.
typescriptif (input.reconnectAttempts < input.maxReconnectAttempts) { return true; } return input.pageHidden;
PonsWarp 적용 지점
| 항목 | 역할 | 증거 |
|---|---|---|
| offset 안전 범위 | 0~totalSize clamp | mobileResumePolicy.ts:49-52 |
| 파일 인덱스 | global offset을 파일 크기로 탐색 | mobileResumePolicy.ts:54-62 |
| fileOffset | 파일 내부 위치 계산 | mobileResumePolicy.ts:66-70 |
| sequence | offset/chunkSize | mobileResumePolicy.ts:70 |
| next partition | 다음 배리어 경계 | mobileResumePolicy.ts:71-74 |
| 재연결 요청 | RESUME_REQUEST | webRTCService.ts:437-450 |
| 복호화 실패 재개 | contiguous frontier부터 요청 | webRTCService.ts:1166-1168 |
global offset을 믿기 전에 검증한다
Resume 커서는 숫자 하나지만, 그 숫자가 유효하려면 파일 목록과 순서가 동일해야 한다. 송신자가 파일을 정렬하는 방식, 수신자가 manifest를 저장한 방식, 파티션 크기, 암호화 모드가 달라지면 같은 offset이 전혀 다른 데이터를 가리킨다. 따라서 resume 요청에는 offset만 보내지 말고 manifest 식별자와 전송 run 정보를 함께 묶어야 한다.
textglobalOffset │ manifest 순서로 파일 누적 크기 검색 ├─ fileIndex + fileOffset ├─ partitionIndex + partitionOffset └─ contiguous frontier 검증 후 재전송 시작
입력은 수신측의 마지막 contiguous frontier다. 상태는 파일 누적 길이와 파티션 경계, 출력은 sender가 다시 읽을 (fileIndex, fileOffset)다. 파일 경계에 정확히 걸리면 다음 파일의 offset 0으로 정규화하고, 중간이면 해당 파일의 상대 offset으로 바꾼다. 이 변환은 파일을 다시 읽는 작업이 아니라 누적 길이를 이용한 탐색이므로 파일 수가 n일 때 단순 구현은 O(n)이다.
처음부터 다시 보내면 안 되나?
작은 파일에서는 처음부터 재전송하는 편이 구현 비용이 낮을 수 있다. 하지만 대용량 전송이나 모바일 재연결에서는 이미 저장한 바이트를 다시 보내는 비용이 크다. 반대로 저장 frontier가 오염됐는데 무조건 이어서 보내면 파일이 조용히 손상된다. Resume는 성능 최적화이면서 동시에 검증 가능한 저장 경계를 요구하는 기능이다.
암호화가 켜져 있으면 nonce/sequence 정책도 재개와 함께 맞아야 한다. 같은 평문 offset을 다시 보내는 것과 같은 암호문 packet을 재사용하는 것은 다른 문제다. 재구축 시 nonce counter, partition sequence, manifest hash를 하나의 resume 계약으로 문서화해야 한다.
| 검증 | 실패 시 행동 |
|---|---|
| manifest 동일성 | resume 거부 후 새 세션 |
| offset 범위 | 0 또는 전체 크기 밖이면 오류 |
| 파일 경계 | 다음 파일 0으로 정규화 |
| contiguous frontier | 구멍이 있으면 가장 앞의 연속 위치 사용 |
| 암호화 상태 | 키·nonce 계약 불일치 시 재개 금지 |
재구축할 때의 체크리스트
수신자는 “마지막으로 받은 패킷”이 아니라 “연속적으로 디스크에 반영된 마지막 위치”를 기록해야 한다. sender는 그 위치부터 읽되, 이미 준비 큐에 남은 패킷을 버리고 새 sequence를 계산해야 한다. 화면이 꺼졌다가 살아난 경우에도 local state만 믿지 말고 peer가 확인한 frontier를 우선한다.
이렇게 하면 Resume 커서는 단순한 seek 위치가 아니라 파일 분할·재정렬·암호화·재전송을 연결하는 복구 계약이 된다.
한 문장 정리
PonsWarp의 Resume 커서는 global offset 하나를 파일 위치·청크 sequence·다음 partition 경계로 변환해 모바일 복귀와 네트워크 끊김 뒤 정확한 지점부터 전송을 이어간다.
관련 개념: [개념 9 — 재정렬 버퍼] · [개념 17 — PARTITION_ACK] · [구현 6 — E2E 암호화]
global offset을 믿기 전에 검증한다
resume 요청의 offset은 네트워크에서 들어온 값이므로 그대로 사용하지 않는다. floor로 정수화하고 0과 totalSize 사이로 clamp한다. 이 검증이 없으면 음수, 파일 끝 초과, 잘못된 sequence가 writer에 들어갈 수 있다.
여러 파일의 경계
파일 A가 700KiB이고 global offset이 900KiB라면 fileIndex는 B가 되고 fileOffset은 200KiB가 된다. 이 변환을 재현하지 못하면 sender는 A의 900KiB를 다시 보내거나 B의 앞부분을 건너뛴다.
textfiles = [A:700KiB, B:500KiB] globalOffset = 900KiB → fileIndex = B → fileOffset = 200KiB
암호화와 resume
암호화 전송에서는 위치만 맞추면 끝나지 않는다. 이미 사용한 nonce counter보다 큰 값에서 다시 시작해야 한다. 그래서 resume cursor 계산과 nonce advance가 함께 움직인다.
모바일 정책은 transfer active, room, writer, fileCount가 모두 있어야 재연결을 계속하고, 시도 예산이 끝나도 pageHidden이면 한동안 생존을 허용한다.