Skip to main content

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.
╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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.
╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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.
╔═════════════════════════════════════════════════════════════════════════╗
║                                                                         ║
║  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.
╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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.
╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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.
╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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?
╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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

╔════════════════════════════════════════════════════════════════════════╗
║                                                                        ║
║  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                               ║
║                                                                        ║
╚════════════════════════════════════════════════════════════════════════╝