
[Blueprint W4] PonsWarp Grid — TS로 뽑아낸 P2P 엔진과 코디네이터
브라우저를 떠난 엔진. PonsWarpEngine, per-piece SHA-256, OPFS 폴백, coordinator planning 분리로 CLI까지 뽑은 TS 엔진.

# [Blueprint W4] PonsWarp Grid — TS로 뽑아낸 P2P 엔진과 코디네이터
> Series: `blueprint-ponswarp` (4/5) · Category: `P2P/분산기술` + `개발 회고` · Slug: `blueprint-ponswarp-04-grid-engine`
> Source: `blueprints/specs/P01_ponswarp_family.md` §04 + `blueprints/04-ponswarp-grid.png` + `ponswarp-grid/packages/core`
—
## 0. TL;DR
`ponswarp`가 브라우저에 종속된 전송이었다면, `grid`는 브라우저를 떠난 엔진이다. `PonsWarpEngine(createSession/joinSession)`이 `piece` 단위로 파일을 쪼개 `per-piece SHA-256`으로 검증하고, `OPFS→IndexedDB→Memory`로 resume한다. `coordinator`는 바이트를 안 만지고 `후보 계획`만 짠다. `Rust/WASM` 없이 순수 TS로 만든 이유: `pnpm`으로 `CLI`까지 뽑기 위해.

—
## 1. 문제 정의 — 왜 엔진을 추출했나
`ponswarp`와 `vnext`는 모두 브라우저 SPA에 묶여 있었다. 그러나 `CLI`에서 `send/join`, `node start`로 시딩하고, `Tauri`에서 `warp-agentd`로 띄우려면, 전송 로직이 브라우저 밖에서도 돌아야 했다. `pons-core-wasm`은 브라우저 전용이라 `Node`에서 못 썼다. 그래서 `grid`는 `Rust`를 버리고 `TS`로 다시 썼다. `grid.ponslink.com`이 그 결과.
—
## 2. 설계도 한눈에 — piece와 스토리지 폴백
중앙 `packages/core: PonsWarpEngine`. 왼쪽 `webrtc`, 오른쪽 `signaling(JS WS :8787)`, 아래 스토리지 3단.
* **piece 흐름:** `createSession`이 파일을 `piece(256KB~1MB)`로 쪼개 `manifest`를 만든다. `joinSession`은 `manifest`를 받아 `per-piece SHA-256` 게이트를 통과한 조각만 `OPFS`에 쓴다. 손상된 piece는 빨간 화살표로 `폐기→재요청`.
* **스토리지:** `OPFS → IndexedDB → Memory` 폴백. `OPFS`가 있으면 파일 핸들로 쓰고, 없으면 `IDB`에 `ArrayBuffer`로 쌓고, 그것도 없으면 메모리에 둔다. `safeAssembleBytes`가 `256MiB`를 기준으로 `Blob` vs `writable stream`을 가른다.
* **coordinator:** `POST /api/grid/v1/*`로 `메타 등록·후보 계획`, `ed25519` 노드 등록. `ponswarp://join` URI로 참여. coordinator는 `누가 어떤 piece를 가졌는지`만 알고, 바이트는 피어끼리 `DataChannel`로 간다.
* **CLI:** `packages/cli(send/join/node start/publish/files/download)`가 `core`를 감싼다. `apps/demo(Vite)`가 브라우저 데모.
—
## 3. 아키텍처 해부 — 3개 패키지
“`
ponswarp-grid/
├── packages/core # PonsWarpEngine, piece 관리, SHA-256
├── packages/webrtc # DataChannel 래퍼
├── packages/signaling # JS WS :8787
├── packages/react # 훅
└── packages/cli # send/join/node
“`
* **core:** `createSession(file) → manifest`, `joinSession(manifest) → session`. `seed-after-complete` — 비소유자도 받은 piece를 provider로 제공한다.
* **signaling:** `Node WS`가 `offer/answer/ICE`만 중계. Rust Axum과 프로토콜은 다르지만, 역할은 동일.
* **coordinator:** `ed25519` 키로 노드를 등록. `planning`은 `coordinator`가, `byte transport`는 `DataChannel`이. 분리 덕에 coordinator는 가볍다.
—
## 4. 로직 딥다이브 — 2개만 깊게
### 4.1 per-piece SHA-256 게이트
“`ts
const hash = await sha256(piece.bytes);
if (hash !== manifest.pieces[seq].sha256) {
discard(piece); requestRetransmit(seq);
}
“`
* 파일 전체 해시가 아니라 `piece`마다 해시. 손상된 piece만 버리고 재요청. `128MB` 파일의 1개 piece가 깨져도 전체를 버리지 않는다.
### 4.2 safeAssembleBytes 256MiB 임계
* `assembleBytes < 256MiB`: `Blob`으로 합쳐 `URL.createObjectURL`로 다운로드.
* `>= 256MiB`: `File System Access API`의 `writable stream`으로 스트리밍 저장. 브라우저 힙을 500MB 이하로 유지하는 `perf:500mb` 검증 통과.
—
## 5. 왜 이 선택인가
| 선택 | 채택 | 버린 대안 | 버린 이유 |
|—|—|—|—|
| 코어 | 순수 TS | Rust/WASM 재사용 | `Node CLI`와 `Tauri`에서 `WASM` 로드가 꼼꼼, `pnpm` 패키지로 뽑기 어려움 |
| 스토리지 | OPFS→IDB→Memory 3단 | OPFS 단일 | `Safari`·`Firefox`에서 `OPFS` 미지원·버그 |
| coordinator | planning vs transport 분리 | coordinator가 바이트 중계 | 대역폭 비용, 단일 장애점 |
—
## 6. 실패와 복구
* **실패:** 처음엔 `piece`를 `64KB`로 고정. `per-piece SHA-256` 검증 오버헤드가 커서 1GB 파일에서 16k번 해시. `256KB~1MB`로 가변으로 바꿔 오버헤드 1/4로 감소.
* **복구:** `perf:500mb` 힙 예산 테스트에서 `Blob` 조립이 500MB를 넘기자 `OOM`. `safeAssembleBytes` 임계를 도입해 스트리밍으로 전환.
—
## 7. 운영 증거
* `pnpm test` — `per-piece SHA-256`, `TURN strict-relay QA`, `heap 500mb` 검증
* `coordinator /api/grid/v1` — `ed25519` 노드 등록, `ponswarp://join` URI, `apps/demo` Vite 데모
* `Docker` — `JS WS :8787` + `coordinator` + `demo` 3종
—
## 8. 다음 편 예고
**W5: `ponswarp-desktop` — WarpInfra.** `Tauri2` + `Rust warp-core` + `QUIC mesh` + `mDNS` + `rarest-first` 스케줄링. 브라우저를 떠난 P2P가 인트라넷에서 어떻게 시딩하는지 다룬다.
*부록: `blueprints/04-ponswarp-grid.png` 프롬프트 `prompts.part1.json#04`, 코드 진입점 `ponswarp-grid/packages/core/src/engine.ts`*

댓글