Epoch Synchronization
EPOCH is MonoTermβs solution for preventing resize race conditions between frontend and backend.The Resize Problem
When a terminal is resized, multiple components must be updated synchronously.Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β TERMINAL RESIZE INVOLVES MULTIPLE COMPONENTS β
β β
β β
β When user resizes the window: β
β β
β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β β β β β β β
β β xterm.js β β Rust β β PTY β β
β β (Frontend) β β (Backend) β β (Daemon) β β
β β β β β β β β
β β 120 x 40 β β 120 x 40 β β 120 x 40 β β
β β β β β β β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β
β β
β All THREE must have the SAME dimensions. β
β If they disagree, rendering will be corrupted. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The Race Condition
Resize notifications travel at different speeds through the system.Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β THE RACE CONDITION β
β β
β β
β Timeline of a resize from 80x24 to 120x40: β
β β
β β
β Time xterm.js Rust Backend PTY/Shell β
β ββββ βββββββββββββ βββββββββββββ ββββββββββ β
β β
β T0 80 x 24 80 x 24 80 x 24 β
β β β β β
β β User resizes β β β
β v window β β β
β T1 120 x 40 β β β
β β β β β
β β Send resize β β β
β β request βββββββΆ β β β
β β v β β
β T2 120 x 40 80 x 24 β β
β β β Processing... β β
β β β β β
β β β β β
β T3 β β GridUpdate β β
β β β (80x24) sent! β β
β β βββββββββββββββ β β β
β β β β β
β β !!! SIZE β β β
β β MISMATCH! β β β
β β 120x40 != β β β
β β 80x24 v β β
β T4 β 120 x 40 β β
β β β Forward to PTY β β
β β β ββββββββββββββββΆ β β
β β β v β
β T5 β β 120 x 40 β
β β β β β
β β β β β
β T6 β β GridUpdate β β
β β βββββββββββββββ β (120x40) sent β β
β β β β β
β β Sizes match OK β β β
β v v v β
β β
β β
β The Problem: β
β β
β At T3, xterm.js receives a GridUpdate with dimensions β
β 80x24, but xterm.js is already 120x40. β
β β
β If we inject this update, text will wrap incorrectly, β
β cursor will be at wrong position, display will be corrupt. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
EPOCH: The Solution
EPOCH is a version number that increments on each resize.Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β EPOCH VERSIONING β
β β
β β
β Concept: β
β β
β - Each resize increments a counter called "EPOCH" β
β - EPOCH is included in every GridUpdate β
β - Frontend rejects updates with old EPOCH β
β β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β Frontend Backend β β
β β β β
β β currentEpoch: 5 β β
β β β β
β β β β β β
β β β User resizes window β β β
β β β β β β
β β β currentEpoch++ (now 6) β β β
β β β β β β
β β β resize_session(epoch: 6) β β β
β β β βββββββββββββββββββββββββββββΆ β β β
β β β β β β
β β β β Store epoch = 6 β β
β β β β β β
β β β GridUpdate (epoch: 5) β β β
β β β ββββββββββββββββββββββββββββ β β β
β β β β β β
β β β 5 < 6 βββΆ DISCARD β β β
β β β (stale update) β β β
β β β β β β
β β β GridUpdate (epoch: 6) β β β
β β β ββββββββββββββββββββββββββββ β β β
β β β β β β
β β β 6 == 6 βββΆ ACCEPT β β β
β β β (current update) β β β
β β β β β β
β β v v β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β β
β Rule: β
β β
β if (update.epoch < currentEpoch) { β
β // This update was generated before the resize β
β // DISCARD IT - dimensions are wrong β
β } β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Frontend-Initiated Pattern
The epoch is managed by the frontend, not the backend.Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β FRONTEND-INITIATED EPOCH PATTERN β
β β
β β
β Why Frontend Controls Epoch: β
β β
β The frontend knows FIRST when a resize happens (xterm.js event). β
β It must increment epoch BEFORE sending resize to backend. β
β This ensures any in-flight GridUpdates are marked as stale. β
β β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β Frontend (xterm.js) Backend (Rust) β β
β β β β
β β 1. Window resize event β β β
β β detected β β β
β β β β β
β β 2. prepareResize() β β β
β β currentEpoch++ β β β
β β (now epoch=6) β β β
β β β β β
β β 3. requestResize(cols, rows, β β β
β β epoch=6) β β β
β β ββββββββββββββββββββββββββββΆ resize(c, r, epoch=6) β β
β β β epoch = 6 β β
β β β β β
β β 4. Any GridUpdate with β β β
β β epoch < 6 is REJECTED β β β
β β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Complete EPOCH Flow
Step-by-step walkthrough of a resize with EPOCH.Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β EPOCH FLOW: RESIZE FROM 80x24 TO 120x40 β
β β
β β
β Frontend Backend PTY β
β (epoch=5) (epoch=5) (80x24) β
β β β β β
β β β β β
β ββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββ β
β 1 β Window resize event β β β
β β xterm now 120x40 β β β
β β β β β
β ββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββ β
β 2 β prepareResize() β β β
β β epoch++ (now 6) β β β
β β β β β
β β requestResize({ β β β
β β cols: 120, β β β
β β rows: 40, β β β
β β epoch: 6 β β β
β β }) ββββββββββββββββββββΆ β β
β β β β β
β ββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββ β
β 3 β β Meanwhile, shell β β
β β β outputs data... β β
β β β β β
β β β GridUpdate (epoch=5) β β
β βββββββββββββββββββββββββ β β
β β β β β
β β DISCARD (5 < 6) β β β
β β β β β
β ββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββ β
β 4 β β Process resize β β
β β β epoch = 6 β β
β β β β β
β β β resize_pty(120,40) ββββΌβββββΆ β
β β β β β
β ββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββ β
β 5 β β β 120x40 β
β β β β β
β β β Shell re-renders... β β
β β βββββββββββββββββββββββββ β
β β β β β
β β β GridUpdate (epoch=6) β β
β βββββββββββββββββββββββββ β β
β β β β β
β β ACCEPT (6 >= 6) β β β
β β Inject to display β β β
β β β β β
β ββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββΌβββββ β
β 6 β Display correct β β β
β β 120x40 content β β β
β β β β β
β v v v β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Double Verification
Even with EPOCH, we verify actual dimensions match.Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β DOUBLE VERIFICATION β
β β
β β
β EPOCH alone is not enough. We also check dimensions: β
β β
β β
β Validation Steps: β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β Step 1: EPOCH Check β β
β β β β
β β update.epoch < currentEpoch ? β β
β β β β
β β YES βββΆ DISCARD (stale) β β
β β NO βββΆ Continue to Step 2 β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β v β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β Step 2: Dimension Check β β
β β β β
β β update.cols == xterm.cols AND β β
β β update.rows == xterm.rows ? β β
β β β β
β β NO βββΆ Request backend resize β β
β β Skip injection β β
β β YES βββΆ Continue to Step 3 β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β v β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β Step 3: Inject to Display β β
β β β β
β β Safe to inject - all checks passed β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β β
β Why Both Checks? β
β β
β EPOCH: Catches updates generated before resize β
β Dimension: Catches unexpected size differences β
β β
β Belt AND suspenders. Both are needed for safety. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why Not Just Use Dimensions?
Why do we need EPOCH when we could just compare dimensions?Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β WHY EPOCH IS NECESSARY β
β β
β β
β Scenario: Rapid Resize β
β β
β User quickly resizes: 80x24 βββΆ 120x40 βββΆ 80x24 β
β β
β β
β Time Action Dimensions β
β ββββ ββββββ ββββββββββ β
β T1 Start 80 x 24 β
β T2 Resize to 120x40 120 x 40 β
β T3 GridUpdate (80x24) in flight ... β
β T4 Resize back to 80x24 80 x 24 β
β T5 GridUpdate (80x24) arrives ... β
β β
β β
β Without EPOCH: β
β β
β At T5, dimensions match (80x24 == 80x24). β
β But this update was generated at T1, before T2's resize! β
β The content is STALE - cursor position is wrong. β
β β
β Dimension check alone: PASS (80 == 80, 24 == 24) β
β But update is stale: WRONG CONTENT! β
β β
β β
β With EPOCH: β
β β
β T1: epoch = 5, generate update (epoch=5, 80x24) β
β T2: epoch++ = 6, resize to 120x40 β
β T4: epoch++ = 7, resize to 80x24 β
β T5: update (epoch=5) arrives, currentEpoch=7 β
β β
β EPOCH check: 5 < 7 βββΆ DISCARD β
β β
β We correctly reject the stale update! β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Summary
Copy
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β EPOCH SYNCHRONIZATION IN ONE DIAGRAM β
β β
β β
β β
β FRONTEND BACKEND β
β ββββββββββββββββββ ββββββββββββββββββ β
β β β β β β
β β currentEpoch β β Grid State β β
β β βββββ β β .epoch β β
β β β 5 β β β βββββ β β
β β βββββ β β β 5 β β β
β β β β β βββββ β β
β β β β β β β β
β β On resize: β β β β β
β β prepareResize()β β β β β
β β epoch++ (now 6)β requestResize() β β β β
β β βββββββββββββββββββΆβ epoch = 6 β β
β β β β β β
β β β GridUpdate β β β β
β β β (epoch: 5) β β β β
β β βββββββββΌβββββββββββββββββββ β β β
β β β β β β β β
β β 5 < 6 ? β β β β β
β β YES βββΆ DROP β β β β β
β β β GridUpdate β β β β
β β β (epoch: 6) β β β β
β β βββββββββΌβββββββββββββββββββ β β β
β β β β β β β β
β β 6 < 6 ? β β β β β
β β NO βββΆ ACCEPTβ β β β β
β β β β β β β β
β β Inject to β β β β β
β β xterm displayβ β β β β
β β β β β β
β ββββββββββββββββββ ββββββββββββββββββ β
β β
β β
β EPOCH = Monotonically increasing version number β
β β
β - Frontend increments on resize β
β - Frontend passes to backend β
β - Backend stores epoch in GridUpdate β
β - Frontend validates before injection β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ