Workspace IndexDev Notes › Your app doesn't read the chain — it reads a projection

#62PoC

Your app doesn't read the chain — it reads a projection

Every real product reads an indexer, not the chain — a derived read model that is always some blocks behind. The honest UI prints which block it is speaking for; the dishonest one just looks occasionally wrong.

Serve the same balance screen twice — one from direct RPC, one from an indexer with injected lag — then add an as-of-block label and a pending-transaction overlay, and watch which version stops looking broken.

Why

Reading the chain directly does not scale past one widget — real screens need joins, history and aggregates, so they read a projection built by an indexer. That is CQRS by necessity, and it imports CQRS's one cost: the read model lags the write model. The user who just paid sees an old balance; the list is missing the newest row.

The failure mode is not the lag — it is hiding it. A screen that says balance as of block 19,342,001, with your payment pending on top, is telling the truth and feels fine. A screen that silently shows stale data feels broken exactly when the user cares most, seconds after they acted. The staleness is unavoidable; the dishonesty is a choice.

How it works

Two data paths for one screen, lag you control, and the two UI elements that repair trust.

PoC

An anvil chain with a token, a tiny indexer (Ponder or a hand-rolled event loop) writing balances to SQLite, and one balance page with a data-source toggle. Add a configurable indexing delay. Make a transfer, watch the indexer-backed view lie for N seconds, then add (1) an as-of block label read from the projection's own cursor and (2) an optimistic pending row sourced from the app's submitted-transactions list. Assert the repaired view never contradicts what the user just did.

What it proves

The projection's lag cannot be engineered away, only labeled and bridged: the as-of label bounds what the screen claims, and the pending overlay covers the gap between the user's action and the indexer's cursor. Every serious dapp front end converges on these two elements — this PoC derives why in fifty lines.

← All Dev Notes · Workspace Index · Top ↑

앱은 체인을 읽지 않는다 — 프로젝션을 읽는다

모든 실제 제품은 체인이 아니라 인덱서 — 항상 몇 블록 뒤처진 파생 읽기 모델 — 를 읽습니다. 정직한 UI 는 자신이 어느 블록을 대변하는지 표기하고, 정직하지 않은 UI 는 그냥 가끔 틀려 보입니다.

같은 잔액 화면을 두 번 서빙합니다 — 하나는 직접 RPC, 하나는 지연을 주입한 인덱서에서. 그다음 기준-블록 라벨과 대기 중 트랜잭션 오버레이를 더하고, 어느 버전이 더는 고장나 보이지 않는지 관찰합니다.

체인을 직접 읽는 방식은 위젯 하나를 넘어서면 확장되지 않습니다 — 실제 화면에는 조인, 이력, 집계가 필요하므로 인덱서가 만든 프로젝션을 읽습니다. 필요에 의한 CQRS 이고, CQRS 의 비용 하나를 그대로 수입합니다: 읽기 모델은 쓰기 모델보다 늦습니다. 방금 결제한 사용자는 옛 잔액을 보고, 목록에는 최신 행이 빠져 있습니다.

실패 모드는 지연이 아니라 지연을 숨기는 것입니다. “블록 19,342,001 기준 잔액” 위에 대기 중인 결제를 얹어 보여주는 화면은 진실을 말하고 있고, 괜찮게 느껴집니다. 낡은 데이터를 말없이 보여주는 화면은 사용자가 가장 신경 쓰는 순간 — 행동한 직후 몇 초 — 에 정확히 고장나 보입니다. 뒤처짐은 불가피하고, 부정직은 선택입니다.

동작 방식

한 화면에 두 데이터 경로, 직접 제어하는 지연, 그리고 신뢰를 복구하는 두 UI 요소.

PoC

토큰이 있는 anvil 체인, 잔액을 SQLite 에 쓰는 작은 인덱서(Ponder 또는 직접 만든 이벤트 루프), 데이터 소스 토글이 있는 잔액 페이지 하나. 설정 가능한 인덱싱 지연을 추가합니다. 전송을 실행해 인덱서 기반 뷰가 N 초간 거짓말하는 것을 본 뒤, (1) 프로젝션 자신의 커서에서 읽은 기준-블록 라벨과 (2) 앱의 제출-트랜잭션 목록에서 가져온 낙관적 대기 행을 더합니다. 복구된 뷰가 사용자의 직전 행동과 절대 모순되지 않음을 확인합니다.

무엇을 증명하나

프로젝션의 지연은 공학으로 없앨 수 없고, 라벨을 붙이고 다리를 놓을 수만 있습니다: 기준 라벨은 화면의 주장 범위를 한정하고, 대기 오버레이는 사용자 행동과 인덱서 커서 사이의 틈을 덮습니다. 진지한 dapp 프런트엔드는 전부 이 두 요소로 수렴합니다 — 이 PoC 는 그 이유를 50줄로 유도합니다.

← 전체 개발 노트 · 워크스페이스 인덱스 · 맨 위 ↑